Browse Source

[qt] Ryujinx save data link (#2815)

This adds an action to the Game List context menu that lets users link
save data from Eden to Ryujinx, or vice versa.

Unfortunately, this isn't so simple to deal with due to the way Ryujinx's saves work. Ryujinx stores its saves in the... config directory... in `bis/user/save`. Unlike Yuzu, however, it doesn't store things by TitleID, instead it's just a bunch of directories from 000...01 to 000...0f and so on. The way it *maps* TitleID to SaveID is via `imkvdb.arc` in `bis/system/save/8000000000000000/0/` and also an identical copy in the `1` directory for... some reason. `imkvdb.arc` is handled by `FlatMapKeyValueStore` in LibHac, which, as the name implies, is a key-value storage system that `imkvdb.arc`, and seemingly `imkvdb.arc` alone, uses. The way this class is written is really weird, almost as if it's designed to accommodate more types of kvdbs... but for now we can safely assume that there aren't gonna be any other `kvdb` implementations added to HorizonNX.

Regardless, the file format is ridiculously simple so I didn't actually need to do a deep dive into C# code... of which I can basically only read Avalonia. A simple `xxd` on the `imkvdb.arc` is all that's needed, and here's everything that matters:
- The `IMKV` magic header (4 bytes)
- 8 bytes that don't really have anything useful to us, except for a size byte (presumably a `u32`) strewn at offset `0x08` from the start of the file, which is useless to us
- Then we start the `IMEN` list. I don't know what the `IM` stands for, but `IMEN` is just, well, an ENtry. Offsets shown are relative to the start of the `IMEN` header.
  * 4-byte `IMEN` magic header at 0x0
  * 8 bytes of filler data. It contains two `0x40` bytes, but I'm not really sure what they do
  * TitleID (u64) at `0xC`, for example `00a0 df10  501f 0001` for Legends: Arceus (the byte order is swapped)
  * 0x38 bytes of filler starting at offset 0x14
  * SaveID (u64) at `0x4C`, for example `0a00 0000 0000 0000` for my Legends: Arceus save
  * 0x38 bytes of filler starting at offset 0x54

Full example for Legends: Arceus:
```
000001b0: 494d 454e 4000 0000 4000 0000 00a0 df10  IMEN@...@.......
000001c0: 501f 0001 0100 0000 0000 0000 0000 0000  P...............
000001d0: 0000 0000 0000 0000 0000 0000 0100 0000  ................
000001e0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000001f0: 0000 0000 0000 0000 0000 0000 0a00 0000  ................
00000200: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000210: 0000 0000 0100 0000 0000 0000 0000 0000  ................
00000220: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000230: 0000 0000 0000 0000 0000 0000 494d 454e  ............IMEN
```
Ultimately, the size of the `IMEN` sits at 0x8C or 140 bytes. With this knowledge reading all the TitleID -> SaveID pairs is basically free, and outside of validation and stuff is like 15 lines of relevant code. Some interesting caveats, though:
- There are two entries for some TitleIDs for... some reason? Ignoring the second one seems to work though.
- Within each save directory, there are directories `0` and `1`... and only `0` ever seems used??? It's where Ryujinx points you to for save, so I just chose to use that.

Once everything is parsed, the rest of the implementation is extremely trivial:
- When the user requests a Ryujinx link, match the current program_id to the corresponding SaveID in `imkvdb`
- If it doesn't exist, just error out (save data is probably nonexistent)
- If it does though, give the user the option to use Eden's current save data OR Ryujinx's current save data.

Old save data is deleted depending on which one you chose.

Signed-off-by: crueter <crueter@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/2815
Reviewed-by: Lizzie <lizzie@eden-emu.dev>
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
pull/2859/head
crueter 2 months ago
parent
commit
39f226a853
No known key found for this signature in database GPG Key ID: 425ACD2D4830EBC6
  1. 2
      src/common/CMakeLists.txt
  2. 1
      src/common/fs/fs_paths.h
  3. 33
      src/common/fs/path_util.cpp
  4. 18
      src/common/fs/path_util.h
  5. 93
      src/common/fs/ryujinx_compat.cpp
  6. 40
      src/common/fs/ryujinx_compat.h
  7. 43
      src/common/fs/symlink.cpp
  8. 12
      src/common/fs/symlink.h
  9. 12
      src/frontend_common/data_manager.cpp
  10. 4
      src/frontend_common/data_manager.h
  11. 3
      src/qt_common/CMakeLists.txt
  12. 2
      src/qt_common/abstract/frontend.cpp
  13. 6
      src/qt_common/abstract/frontend.h
  14. 10
      src/qt_common/qt_common.cpp
  15. 23
      src/qt_common/qt_string_lookup.h
  16. 6
      src/qt_common/util/content.cpp
  17. 6
      src/qt_common/util/content.h
  18. 130
      src/qt_common/util/fs.cpp
  19. 22
      src/qt_common/util/fs.h
  20. 2
      src/qt_common/util/game.cpp
  21. 2
      src/qt_common/util/path.cpp
  22. 1
      src/yuzu/CMakeLists.txt
  23. 2
      src/yuzu/data_dialog.cpp
  24. 5
      src/yuzu/game_list.cpp
  25. 1
      src/yuzu/game_list.h
  26. 61
      src/yuzu/main.cpp
  27. 3
      src/yuzu/main.h
  28. 9
      src/yuzu/migration_worker.cpp
  29. 6
      src/yuzu/migration_worker.h
  30. 40
      src/yuzu/ryujinx_dialog.cpp
  31. 32
      src/yuzu/ryujinx_dialog.h
  32. 81
      src/yuzu/ryujinx_dialog.ui

2
src/common/CMakeLists.txt

@ -153,6 +153,8 @@ add_library(
wall_clock.h wall_clock.h
zstd_compression.cpp zstd_compression.cpp
zstd_compression.h zstd_compression.h
fs/ryujinx_compat.h fs/ryujinx_compat.cpp
fs/symlink.h fs/symlink.cpp
) )
if(WIN32) if(WIN32)

1
src/common/fs/fs_paths.h

@ -33,6 +33,7 @@
#define SUDACHI_DIR "sudachi" #define SUDACHI_DIR "sudachi"
#define YUZU_DIR "yuzu" #define YUZU_DIR "yuzu"
#define SUYU_DIR "suyu" #define SUYU_DIR "suyu"
#define RYUJINX_DIR "Ryujinx"
// yuzu-specific files // yuzu-specific files
#define LOG_FILE "eden_log.txt" #define LOG_FILE "eden_log.txt"

33
src/common/fs/path_util.cpp

@ -84,7 +84,7 @@ public:
return eden_paths.at(eden_path); return eden_paths.at(eden_path);
} }
[[nodiscard]] const fs::path& GetLegacyPathImpl(LegacyPath legacy_path) {
[[nodiscard]] const fs::path& GetLegacyPathImpl(EmuPath legacy_path) {
return legacy_paths.at(legacy_path); return legacy_paths.at(legacy_path);
} }
@ -98,7 +98,7 @@ public:
eden_paths.insert_or_assign(eden_path, new_path); eden_paths.insert_or_assign(eden_path, new_path);
} }
void SetLegacyPathImpl(LegacyPath legacy_path, const fs::path& new_path) {
void SetLegacyPathImpl(EmuPath legacy_path, const fs::path& new_path) {
legacy_paths.insert_or_assign(legacy_path, new_path); legacy_paths.insert_or_assign(legacy_path, new_path);
} }
@ -118,9 +118,9 @@ public:
} }
eden_path_cache = eden_path / CACHE_DIR; eden_path_cache = eden_path / CACHE_DIR;
eden_path_config = eden_path / CONFIG_DIR; eden_path_config = eden_path / CONFIG_DIR;
#define LEGACY_PATH(titleName, upperName) GenerateLegacyPath(LegacyPath::titleName##Dir, GetAppDataRoamingDirectory() / upperName##_DIR); \
GenerateLegacyPath(LegacyPath::titleName##ConfigDir, GetAppDataRoamingDirectory() / upperName##_DIR / CONFIG_DIR); \
GenerateLegacyPath(LegacyPath::titleName##CacheDir, GetAppDataRoamingDirectory() / upperName##_DIR / CACHE_DIR);
#define LEGACY_PATH(titleName, upperName) GenerateLegacyPath(EmuPath::titleName##Dir, GetAppDataRoamingDirectory() / upperName##_DIR); \
GenerateLegacyPath(EmuPath::titleName##ConfigDir, GetAppDataRoamingDirectory() / upperName##_DIR / CONFIG_DIR); \
GenerateLegacyPath(EmuPath::titleName##CacheDir, GetAppDataRoamingDirectory() / upperName##_DIR / CACHE_DIR);
LEGACY_PATH(Citron, CITRON) LEGACY_PATH(Citron, CITRON)
LEGACY_PATH(Sudachi, SUDACHI) LEGACY_PATH(Sudachi, SUDACHI)
LEGACY_PATH(Yuzu, YUZU) LEGACY_PATH(Yuzu, YUZU)
@ -140,9 +140,9 @@ public:
eden_path_cache = eden_path / CACHE_DIR; eden_path_cache = eden_path / CACHE_DIR;
eden_path_config = eden_path / CONFIG_DIR; eden_path_config = eden_path / CONFIG_DIR;
} }
#define LEGACY_PATH(titleName, upperName) GenerateLegacyPath(LegacyPath::titleName##Dir, GetDataDirectory("XDG_DATA_HOME") / upperName##_DIR); \
GenerateLegacyPath(LegacyPath::titleName##ConfigDir, GetDataDirectory("XDG_CONFIG_HOME") / upperName##_DIR); \
GenerateLegacyPath(LegacyPath::titleName##CacheDir, GetDataDirectory("XDG_CACHE_HOME") / upperName##_DIR);
#define LEGACY_PATH(titleName, upperName) GenerateLegacyPath(EmuPath::titleName##Dir, GetDataDirectory("XDG_DATA_HOME") / upperName##_DIR); \
GenerateLegacyPath(EmuPath::titleName##ConfigDir, GetDataDirectory("XDG_CONFIG_HOME") / upperName##_DIR); \
GenerateLegacyPath(EmuPath::titleName##CacheDir, GetDataDirectory("XDG_CACHE_HOME") / upperName##_DIR);
LEGACY_PATH(Citron, CITRON) LEGACY_PATH(Citron, CITRON)
LEGACY_PATH(Sudachi, SUDACHI) LEGACY_PATH(Sudachi, SUDACHI)
LEGACY_PATH(Yuzu, YUZU) LEGACY_PATH(Yuzu, YUZU)
@ -165,6 +165,15 @@ public:
GenerateEdenPath(EdenPath::ShaderDir, eden_path / SHADER_DIR); GenerateEdenPath(EdenPath::ShaderDir, eden_path / SHADER_DIR);
GenerateEdenPath(EdenPath::TASDir, eden_path / TAS_DIR); GenerateEdenPath(EdenPath::TASDir, eden_path / TAS_DIR);
GenerateEdenPath(EdenPath::IconsDir, eden_path / ICONS_DIR); GenerateEdenPath(EdenPath::IconsDir, eden_path / ICONS_DIR);
#ifdef _WIN32
GenerateLegacyPath(EmuPath::RyujinxDir, GetAppDataRoamingDirectory() / RYUJINX_DIR);
#else
// In Ryujinx's infinite wisdom, it places EVERYTHING in the config directory on UNIX
// This is incredibly stupid and violates a million XDG standards, but whatever
GenerateLegacyPath(EmuPath::RyujinxDir, GetDataDirectory("XDG_CONFIG_HOME") / RYUJINX_DIR);
#endif
} }
private: private:
@ -179,12 +188,12 @@ private:
SetEdenPathImpl(eden_path, new_path); SetEdenPathImpl(eden_path, new_path);
} }
void GenerateLegacyPath(LegacyPath legacy_path, const fs::path& new_path) {
void GenerateLegacyPath(EmuPath legacy_path, const fs::path& new_path) {
SetLegacyPathImpl(legacy_path, new_path); SetLegacyPathImpl(legacy_path, new_path);
} }
std::unordered_map<EdenPath, fs::path> eden_paths; std::unordered_map<EdenPath, fs::path> eden_paths;
std::unordered_map<LegacyPath, fs::path> legacy_paths;
std::unordered_map<EmuPath, fs::path> legacy_paths;
}; };
bool ValidatePath(const fs::path& path) { bool ValidatePath(const fs::path& path) {
@ -272,7 +281,7 @@ const fs::path& GetEdenPath(EdenPath eden_path) {
return PathManagerImpl::GetInstance().GetEdenPathImpl(eden_path); return PathManagerImpl::GetInstance().GetEdenPathImpl(eden_path);
} }
const std::filesystem::path& GetLegacyPath(LegacyPath legacy_path) {
const std::filesystem::path& GetLegacyPath(EmuPath legacy_path) {
return PathManagerImpl::GetInstance().GetLegacyPathImpl(legacy_path); return PathManagerImpl::GetInstance().GetLegacyPathImpl(legacy_path);
} }
@ -280,7 +289,7 @@ std::string GetEdenPathString(EdenPath eden_path) {
return PathToUTF8String(GetEdenPath(eden_path)); return PathToUTF8String(GetEdenPath(eden_path));
} }
std::string GetLegacyPathString(LegacyPath legacy_path) {
std::string GetLegacyPathString(EmuPath legacy_path) {
return PathToUTF8String(GetLegacyPath(legacy_path)); return PathToUTF8String(GetLegacyPath(legacy_path));
} }

18
src/common/fs/path_util.h

@ -32,22 +32,26 @@ enum class EdenPath {
IconsDir, // Where Icons for Windows shortcuts are stored. IconsDir, // Where Icons for Windows shortcuts are stored.
}; };
enum LegacyPath {
CitronDir, // Citron Directories for migration
// migration/compat dirs
enum EmuPath {
CitronDir,
CitronConfigDir, CitronConfigDir,
CitronCacheDir, CitronCacheDir,
SudachiDir, // Sudachi Directories for migration
SudachiDir,
SudachiConfigDir, SudachiConfigDir,
SudachiCacheDir, SudachiCacheDir,
YuzuDir, // Yuzu Directories for migration
YuzuDir,
YuzuConfigDir, YuzuConfigDir,
YuzuCacheDir, YuzuCacheDir,
SuyuDir, // Suyu Directories for migration
SuyuDir,
SuyuConfigDir, SuyuConfigDir,
SuyuCacheDir, SuyuCacheDir,
// used exclusively for save data linking
RyujinxDir,
}; };
/** /**
@ -229,7 +233,7 @@ void SetAppDirectory(const std::string& app_directory);
* *
* @returns The filesystem path associated with the LegacyPath enum. * @returns The filesystem path associated with the LegacyPath enum.
*/ */
[[nodiscard]] const std::filesystem::path& GetLegacyPath(LegacyPath legacy_path);
[[nodiscard]] const std::filesystem::path& GetLegacyPath(EmuPath legacy_path);
/** /**
* Gets the filesystem path associated with the EdenPath enum as a UTF-8 encoded std::string. * Gets the filesystem path associated with the EdenPath enum as a UTF-8 encoded std::string.
@ -247,7 +251,7 @@ void SetAppDirectory(const std::string& app_directory);
* *
* @returns The filesystem path associated with the LegacyPath enum as a UTF-8 encoded std::string. * @returns The filesystem path associated with the LegacyPath enum as a UTF-8 encoded std::string.
*/ */
[[nodiscard]] std::string GetLegacyPathString(LegacyPath legacy_path);
[[nodiscard]] std::string GetLegacyPathString(EmuPath legacy_path);
/** /**
* Sets a new filesystem path associated with the EdenPath enum. * Sets a new filesystem path associated with the EdenPath enum.

93
src/common/fs/ryujinx_compat.cpp

@ -0,0 +1,93 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include "ryujinx_compat.h"
#include "common/fs/path_util.h"
#include <cstddef>
#include <cstring>
#include <fmt/ranges.h>
#include <fstream>
namespace Common::FS {
namespace fs = std::filesystem;
fs::path GetKvdbPath()
{
return GetLegacyPath(EmuPath::RyujinxDir) / "bis" / "system" / "save" / "8000000000000000" / "0"
/ "imkvdb.arc";
}
fs::path GetRyuSavePath(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";
}
IMENReadResult ReadKvdb(const fs::path &path, std::vector<IMEN> &imens)
{
std::ifstream kvdb{path, std::ios::binary | std::ios::ate};
if (!kvdb) {
return IMENReadResult::Nonexistent;
}
size_t file_size = kvdb.tellg();
// IMKV header + 8 bytes
if (file_size < 0xB) {
return IMENReadResult::NoHeader;
}
// magic (not the wizard kind)
kvdb.seekg(0, std::ios::beg);
char header[12];
kvdb.read(header, 12);
if (std::memcmp(header, IMKV_MAGIC, 4) != 0) {
return IMENReadResult::InvalidMagic;
}
// calculate num. of imens left
std::size_t remaining = (file_size - 12);
std::size_t num_imens = remaining / IMEN_SIZE;
// File is misaligned and probably corrupt (rip)
if (remaining % IMEN_SIZE != 0) {
return IMENReadResult::Misaligned;
}
// if there aren't any IMENs, it's empty and we can safely no-op out of here
if (num_imens == 0) {
return IMENReadResult::NoImens;
}
imens.reserve(num_imens);
// initially I wanted to do a struct, but imkvdb is 140 bytes
// while the compiler will murder you if you try to align u64 to 4 bytes
for (std::size_t i = 0; i < num_imens; ++i) {
char magic[4];
u64 title_id = 0;
u64 save_id = 0;
kvdb.read(magic, 4);
if (std::memcmp(magic, IMEN_MAGIC, 4) != 0) {
return IMENReadResult::InvalidMagic;
}
kvdb.ignore(0x8);
kvdb.read(reinterpret_cast<char *>(&title_id), 8);
kvdb.ignore(0x38);
kvdb.read(reinterpret_cast<char *>(&save_id), 8);
kvdb.ignore(0x38);
imens.emplace_back(IMEN{title_id, save_id});
}
return IMENReadResult::Success;
}
} // namespace Common::FS

40
src/common/fs/ryujinx_compat.h

@ -0,0 +1,40 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include "common/common_types.h"
#include <filesystem>
#include <vector>
namespace fs = std::filesystem;
namespace Common::FS {
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);
enum class IMENReadResult {
Nonexistent, // ryujinx not found
NoHeader, // file isn't big enough for header
InvalidMagic, // no IMKV or IMEN header
Misaligned, // file isn't aligned to expected IMEN boundaries
NoImens, // no-op, there are no IMENs
Success, // :)
};
struct IMEN
{
u64 title_id;
u64 save_id;
};
static_assert(sizeof(IMEN) == 0x10, "IMEN has incorrect size.");
IMENReadResult ReadKvdb(const fs::path &path, std::vector<IMEN> &imens);
} // namespace Common::FS

43
src/common/fs/symlink.cpp

@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include "symlink.h"
#ifdef _WIN32
#include <windows.h>
#include <fmt/format.h>
#endif
namespace fs = std::filesystem;
// The sole purpose of this file is to treat symlinks like symlinks on POSIX,
// or treat them as directory junctions on Windows.
// 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.
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());
return system(command.c_str()) == 0;
#else
std::error_code ec;
fs::create_directory_symlink(from, to, ec);
return !ec;
#endif
}
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
}
} // namespace Common::FS

12
src/common/fs/symlink.h

@ -0,0 +1,12 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <filesystem>
namespace Common::FS {
bool CreateSymlink(const std::filesystem::path &from, const std::filesystem::path &to);
bool IsSymlink(const std::filesystem::path &path);
} // namespace Common::FS

12
src/frontend_common/data_manager.cpp

@ -4,14 +4,13 @@
#include "data_manager.h" #include "data_manager.h"
#include "common/assert.h" #include "common/assert.h"
#include "common/fs/path_util.h" #include "common/fs/path_util.h"
#include <filesystem>
#include <fmt/format.h> #include <fmt/format.h>
namespace FrontendCommon::DataManager { namespace FrontendCommon::DataManager {
namespace fs = std::filesystem; namespace fs = std::filesystem;
const std::string GetDataDir(DataDir dir, const std::string &user_id)
const fs::path GetDataDir(DataDir dir, const std::string &user_id)
{ {
const fs::path nand_dir = Common::FS::GetEdenPath(Common::FS::EdenPath::NANDDir); const fs::path nand_dir = Common::FS::GetEdenPath(Common::FS::EdenPath::NANDDir);
@ -35,6 +34,11 @@ const std::string GetDataDir(DataDir dir, const std::string &user_id)
return ""; return "";
} }
const std::string GetDataDirString(DataDir dir, const std::string &user_id)
{
return GetDataDir(dir, user_id).string();
}
u64 ClearDir(DataDir dir, const std::string &user_id) u64 ClearDir(DataDir dir, const std::string &user_id)
{ {
fs::path data_dir = GetDataDir(dir, user_id); fs::path data_dir = GetDataDir(dir, user_id);
@ -65,7 +69,7 @@ u64 DataDirSize(DataDir dir)
if (!fs::exists(data_dir)) if (!fs::exists(data_dir))
return 0; return 0;
for (const auto& entry : fs::recursive_directory_iterator(data_dir)) {
for (const auto &entry : fs::recursive_directory_iterator(data_dir)) {
if (!entry.is_directory()) { if (!entry.is_directory()) {
size += entry.file_size(); size += entry.file_size();
} }
@ -74,4 +78,4 @@ u64 DataDirSize(DataDir dir)
return size; return size;
} }
}
} // namespace FrontendCommon::DataManager

4
src/frontend_common/data_manager.h

@ -6,12 +6,14 @@
#include "common/common_types.h" #include "common/common_types.h"
#include <string> #include <string>
#include <filesystem>
namespace FrontendCommon::DataManager { namespace FrontendCommon::DataManager {
enum class DataDir { Saves, UserNand, SysNand, Mods, Shaders }; enum class DataDir { Saves, UserNand, SysNand, Mods, Shaders };
const std::string GetDataDir(DataDir dir, const std::string &user_id = "");
const std::filesystem::path GetDataDir(DataDir dir, const std::string &user_id = "");
const std::string GetDataDirString(DataDir dir, const std::string &user_id = "");
u64 ClearDir(DataDir dir, const std::string &user_id = ""); u64 ClearDir(DataDir dir, const std::string &user_id = "");

3
src/qt_common/CMakeLists.txt

@ -20,13 +20,14 @@ add_library(qt_common STATIC
util/applet.h util/applet.cpp util/applet.h util/applet.cpp
util/compress.h util/compress.cpp util/compress.h util/compress.cpp
abstract/qt_frontend_util.h abstract/qt_frontend_util.cpp
abstract/frontend.h abstract/frontend.cpp
abstract/qt_progress_dialog.h abstract/qt_progress_dialog.cpp abstract/qt_progress_dialog.h abstract/qt_progress_dialog.cpp
qt_string_lookup.h qt_string_lookup.h
qt_compat.h qt_compat.h
discord/discord.h discord/discord.h
util/fs.h util/fs.cpp
) )
create_target_directory_groups(qt_common) create_target_directory_groups(qt_common)

2
src/qt_common/abstract/qt_frontend_util.cpp → src/qt_common/abstract/frontend.cpp

@ -1,7 +1,7 @@
// 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_frontend_util.h"
#include "frontend.h"
#include "qt_common/qt_common.h" #include "qt_common/qt_common.h"
#ifdef YUZU_QT_WIDGETS #ifdef YUZU_QT_WIDGETS

6
src/qt_common/abstract/qt_frontend_util.h → src/qt_common/abstract/frontend.h

@ -1,8 +1,8 @@
// 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
#ifndef QT_FRONTEND_UTIL_H
#define QT_FRONTEND_UTIL_H
#ifndef FRONTEND_H
#define FRONTEND_H
#include <QGuiApplication> #include <QGuiApplication>
#include "qt_common/qt_common.h" #include "qt_common/qt_common.h"
@ -136,4 +136,4 @@ const QString GetSaveFileName(const QString &title,
Options options = Options()); Options options = Options());
} // namespace QtCommon::Frontend } // namespace QtCommon::Frontend
#endif // QT_FRONTEND_UTIL_H
#endif // FRONTEND_H

10
src/qt_common/qt_common.cpp

@ -3,11 +3,14 @@
#include "qt_common.h" #include "qt_common.h"
#include "common/fs/fs.h" #include "common/fs/fs.h"
#include "common/fs/ryujinx_compat.h"
#include <QGuiApplication> #include <QGuiApplication>
#include <QStringLiteral> #include <QStringLiteral>
#include "common/logging/log.h" #include "common/logging/log.h"
#include "core/frontend/emu_window.h" #include "core/frontend/emu_window.h"
#include "qt_common/abstract/frontend.h"
#include "qt_common/qt_string_lookup.h"
#include <QFile> #include <QFile>
@ -33,7 +36,8 @@ std::unique_ptr<Core::System> system = nullptr;
std::shared_ptr<FileSys::RealVfsFilesystem> vfs = nullptr; std::shared_ptr<FileSys::RealVfsFilesystem> vfs = nullptr;
std::unique_ptr<FileSys::ManualContentProvider> provider = nullptr; std::unique_ptr<FileSys::ManualContentProvider> provider = nullptr;
Core::Frontend::WindowSystemType GetWindowSystemType() {
Core::Frontend::WindowSystemType GetWindowSystemType()
{
// Determine WSI type based on Qt platform. // Determine WSI type based on Qt platform.
QString platform_name = QGuiApplication::platformName(); QString platform_name = QGuiApplication::platformName();
if (platform_name == QStringLiteral("windows")) if (platform_name == QStringLiteral("windows"))
@ -101,9 +105,11 @@ void Init(QObject* root)
provider = std::make_unique<FileSys::ManualContentProvider>(); provider = std::make_unique<FileSys::ManualContentProvider>();
} }
std::filesystem::path GetEdenCommand() {
std::filesystem::path GetEdenCommand()
{
std::filesystem::path command; std::filesystem::path command;
// TODO: flatpak?
QString appimage = QString::fromLocal8Bit(getenv("APPIMAGE")); QString appimage = QString::fromLocal8Bit(getenv("APPIMAGE"));
if (!appimage.isEmpty()) { if (!appimage.isEmpty()) {
command = std::filesystem::path{appimage.toStdString()}; command = std::filesystem::path{appimage.toStdString()};

23
src/qt_common/qt_string_lookup.h

@ -42,9 +42,18 @@ enum StringKey {
MigrationTooltipClearOld, MigrationTooltipClearOld,
MigrationTooltipLinkOld, MigrationTooltipLinkOld,
// ryujinx
KvdbNonexistent,
KvdbNoHeader,
KvdbInvalidMagic,
KvdbMisaligned,
KvdbNoImens,
RyujinxNoSaveId,
}; };
static const frozen::map<StringKey, frozen::string, 21> strings = {
static const constexpr frozen::map<StringKey, frozen::string, 27> strings = {
// 0-4
{SavesTooltip, {SavesTooltip,
QT_TR_NOOP("Contains game save data. DO NOT REMOVE UNLESS YOU KNOW WHAT YOU'RE DOING!")}, QT_TR_NOOP("Contains game save data. DO NOT REMOVE UNLESS YOU KNOW WHAT YOU'RE DOING!")},
{ShadersTooltip, {ShadersTooltip,
@ -54,6 +63,7 @@ static const frozen::map<StringKey, frozen::string, 21> strings = {
{ModsTooltip, QT_TR_NOOP("Contains game mods, patches, and cheats.")}, {ModsTooltip, QT_TR_NOOP("Contains game mods, patches, and cheats.")},
// Key install // Key install
// 5-9
{KeyInstallSuccess, QT_TR_NOOP("Decryption Keys were successfully installed")}, {KeyInstallSuccess, QT_TR_NOOP("Decryption Keys were successfully installed")},
{KeyInstallInvalidDir, QT_TR_NOOP("Unable to read key directory, aborting")}, {KeyInstallInvalidDir, QT_TR_NOOP("Unable to read key directory, aborting")},
{KeyInstallErrorFailedCopy, QT_TR_NOOP("One or more keys failed to copy.")}, {KeyInstallErrorFailedCopy, QT_TR_NOOP("One or more keys failed to copy.")},
@ -65,6 +75,7 @@ static const frozen::map<StringKey, frozen::string, 21> strings = {
"re-dump keys.")}, "re-dump keys.")},
// fw install // fw install
// 10-14
{FwInstallSuccess, QT_TR_NOOP("Successfully installed firmware version %1")}, {FwInstallSuccess, QT_TR_NOOP("Successfully installed firmware version %1")},
{FwInstallNoNCAs, QT_TR_NOOP("Unable to locate potential firmware NCA files")}, {FwInstallNoNCAs, QT_TR_NOOP("Unable to locate potential firmware NCA files")},
{FwInstallFailedDelete, QT_TR_NOOP("Failed to delete one or more firmware files.")}, {FwInstallFailedDelete, QT_TR_NOOP("Failed to delete one or more firmware files.")},
@ -75,6 +86,7 @@ static const frozen::map<StringKey, frozen::string, 21> strings = {
"Eden or re-install firmware.")}, "Eden or re-install firmware.")},
// migrator // migrator
// 15-20
{MigrationPromptPrefix, QT_TR_NOOP("Eden has detected user data for the following emulators:")}, {MigrationPromptPrefix, QT_TR_NOOP("Eden has detected user data for the following emulators:")},
{MigrationPrompt, {MigrationPrompt,
QT_TR_NOOP("Would you like to migrate your data for use in Eden?\n" QT_TR_NOOP("Would you like to migrate your data for use in Eden?\n"
@ -93,6 +105,15 @@ static const frozen::map<StringKey, frozen::string, 21> strings = {
{MigrationTooltipLinkOld, {MigrationTooltipLinkOld,
QT_TR_NOOP("Creates a filesystem link between the old directory and Eden directory.\n" QT_TR_NOOP("Creates a filesystem link between the old directory and Eden directory.\n"
"This is recommended if you want to share data between emulators.")}, "This is recommended if you want to share data between emulators.")},
// why am I writing these comments again
// 21-26
{KvdbNonexistent, QT_TR_NOOP("Ryujinx title database does not exist.")},
{KvdbNoHeader, QT_TR_NOOP("Invalid header on Ryujinx title database.")},
{KvdbInvalidMagic, QT_TR_NOOP("Invalid magic header on Ryujinx title database.")},
{KvdbMisaligned, QT_TR_NOOP("Invalid byte alignment on Ryujinx title database.")},
{KvdbNoImens, QT_TR_NOOP("No items found in Ryujinx title database.")},
{RyujinxNoSaveId, QT_TR_NOOP("Title %1 not found in Ryujinx title database.")},
}; };
static inline const QString Lookup(StringKey key) static inline const QString Lookup(StringKey key)

6
src/qt_common/util/content.cpp

@ -11,7 +11,7 @@
#include "frontend_common/firmware_manager.h" #include "frontend_common/firmware_manager.h"
#include "compress.h" #include "compress.h"
#include "qt_common/abstract/qt_frontend_util.h"
#include "qt_common/abstract/frontend.h"
#include "qt_common/abstract/qt_progress_dialog.h" #include "qt_common/abstract/qt_progress_dialog.h"
#include "qt_common/qt_common.h" #include "qt_common/qt_common.h"
@ -404,7 +404,7 @@ void ExportDataDir(FrontendCommon::DataManager::DataDir data_dir,
std::function<void()> callback) std::function<void()> callback)
{ {
using namespace QtCommon::Frontend; using namespace QtCommon::Frontend;
const std::string dir = FrontendCommon::DataManager::GetDataDir(data_dir, user_id);
const std::string dir = FrontendCommon::DataManager::GetDataDirString(data_dir, user_id);
const QString zip_dump_location = GetSaveFileName(tr("Select Export Location"), const QString zip_dump_location = GetSaveFileName(tr("Select Export Location"),
tr("%1.zip").arg(name), tr("%1.zip").arg(name),
@ -468,7 +468,7 @@ void ImportDataDir(FrontendCommon::DataManager::DataDir data_dir,
const std::string& user_id, const std::string& user_id,
std::function<void()> callback) std::function<void()> callback)
{ {
const std::string dir = FrontendCommon::DataManager::GetDataDir(data_dir, user_id);
const std::string dir = FrontendCommon::DataManager::GetDataDirString(data_dir, user_id);
using namespace QtCommon::Frontend; using namespace QtCommon::Frontend;

6
src/qt_common/util/content.h

@ -25,7 +25,8 @@ enum class FirmwareInstallResult {
inline const QString GetFirmwareInstallResultString(FirmwareInstallResult result) inline const QString GetFirmwareInstallResultString(FirmwareInstallResult result)
{ {
return QtCommon::StringLookup::Lookup(static_cast<StringLookup::StringKey>((int) result + (int) QtCommon::StringLookup::FwInstallSuccess));
return QtCommon::StringLookup::Lookup(static_cast<StringLookup::StringKey>(
(int) result + (int) QtCommon::StringLookup::FwInstallSuccess));
} }
/** /**
@ -36,7 +37,8 @@ inline const QString GetFirmwareInstallResultString(FirmwareInstallResult result
inline const QString GetKeyInstallResultString(FirmwareManager::KeyInstallResult result) inline const QString GetKeyInstallResultString(FirmwareManager::KeyInstallResult result)
{ {
// this can probably be made into a common function of sorts // this can probably be made into a common function of sorts
return QtCommon::StringLookup::Lookup(static_cast<StringLookup::StringKey>((int) result + (int) QtCommon::StringLookup::KeyInstallSuccess));
return QtCommon::StringLookup::Lookup(static_cast<StringLookup::StringKey>(
(int) result + (int) QtCommon::StringLookup::KeyInstallSuccess));
} }
void InstallFirmware(const QString &location, bool recursive); void InstallFirmware(const QString &location, bool recursive);

130
src/qt_common/util/fs.cpp

@ -0,0 +1,130 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <filesystem>
#include "fs.h"
#include "common/fs/ryujinx_compat.h"
#include "common/fs/symlink.h"
#include "frontend_common/data_manager.h"
#include "qt_common/abstract/frontend.h"
#include "qt_common/qt_string_lookup.h"
namespace fs = std::filesystem;
namespace QtCommon::FS {
void LinkRyujinx(std::filesystem::path &from, std::filesystem::path &to)
{
std::error_code ec;
// "ignore" errors--if the dir fails to be deleted, error handling later will handle it
fs::remove_all(to, ec);
if (Common::FS::CreateSymlink(from, to)) {
QtCommon::Frontend::Information(tr("Linked Save Data"), tr("Save data has been linked."));
} else {
QtCommon::Frontend::Critical(
tr("Failed to link save data"),
tr("Could not link directory:\n\t%1\nTo:\n\t%2").arg(QString::fromStdString(from.string()), QString::fromStdString(to.string())));
}
}
bool CheckUnlink(const fs::path &eden_dir, const fs::path &ryu_dir)
{
bool eden_link = Common::FS::IsSymlink(eden_dir);
bool ryu_link = Common::FS::IsSymlink(ryu_dir);
if (!(eden_link || ryu_link))
return false;
auto result = QtCommon::Frontend::Warning(
tr("Already Linked"),
tr("This title is already linked to Ryujinx. Would you like to unlink it?"),
QtCommon::Frontend::StandardButton::Yes | QtCommon::Frontend::StandardButton::No);
if (result != QtCommon::Frontend::StandardButton::Yes)
return true;
fs::path linked;
fs::path orig;
if (eden_link) {
linked = eden_dir;
orig = ryu_dir;
} else {
linked = ryu_dir;
orig = eden_dir;
}
// first cleanup the symlink/junction,
try {
// NB: do NOT use remove_all, as Windows treats this as a remove_all to the target,
// NOT the junction
fs::remove(linked);
} catch (std::exception &e) {
QtCommon::Frontend::Critical(
tr("Failed to unlink old directory"),
tr("OS returned error: %1").arg(QString::fromStdString(e.what())));
return true;
}
// then COPY the other dir
try {
fs::copy(orig, linked, fs::copy_options::recursive);
} catch (std::exception &e) {
QtCommon::Frontend::Critical(
tr("Failed to copy save data"),
tr("OS returned error: %1").arg(QString::fromStdString(e.what())));
}
QtCommon::Frontend::Information(
tr("Unlink Successful"),
tr("Successfully unlinked Ryujinx save data. Save data has been kept intact."));
return true;
}
u64 GetRyujinxSaveID(const u64 &program_id)
{
auto path = Common::FS::GetKvdbPath();
std::vector<Common::FS::IMEN> imens;
Common::FS::IMENReadResult res = Common::FS::ReadKvdb(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;
}
QtCommon::Frontend::Critical(
tr("Could not find Ryujinx save data"),
StringLookup::Lookup(StringLookup::RyujinxNoSaveId).arg(program_id, 0, 16));
} else {
// TODO: make this long thing a function or something
QString caption = StringLookup::Lookup(
static_cast<StringLookup::StringKey>((int) res + (int) StringLookup::KvdbNonexistent));
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);
}
} // namespace QtCommon::FS

22
src/qt_common/util/fs.h

@ -0,0 +1,22 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#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);
/// returns FALSE if the dirs are NOT linked
bool CheckUnlink(const std::filesystem::path &eden_dir, const std::filesystem::path &ryu_dir);
} // namespace QtCommon::FS

2
src/qt_common/util/game.cpp

@ -8,7 +8,7 @@
#include "core/file_sys/savedata_factory.h" #include "core/file_sys/savedata_factory.h"
#include "core/hle/service/am/am_types.h" #include "core/hle/service/am/am_types.h"
#include "frontend_common/content_manager.h" #include "frontend_common/content_manager.h"
#include "qt_common/abstract/qt_frontend_util.h"
#include "qt_common/abstract/frontend.h"
#include "qt_common/config/uisettings.h" #include "qt_common/config/uisettings.h"
#include "qt_common/qt_common.h" #include "qt_common/qt_common.h"
#include "yuzu/util/util.h" #include "yuzu/util/util.h"

2
src/qt_common/util/path.cpp

@ -7,7 +7,7 @@
#include <QUrl> #include <QUrl>
#include "common/fs/fs.h" #include "common/fs/fs.h"
#include "common/fs/path_util.h" #include "common/fs/path_util.h"
#include "qt_common/abstract/qt_frontend_util.h"
#include "qt_common/abstract/frontend.h"
#include <fmt/format.h> #include <fmt/format.h>
namespace QtCommon::Path { namespace QtCommon::Path {

1
src/yuzu/CMakeLists.txt

@ -236,6 +236,7 @@ add_executable(yuzu
data_dialog.h data_dialog.cpp data_dialog.ui data_dialog.h data_dialog.cpp data_dialog.ui
data_widget.ui data_widget.ui
ryujinx_dialog.h ryujinx_dialog.cpp ryujinx_dialog.ui
) )
set_target_properties(yuzu PROPERTIES OUTPUT_NAME "eden") set_target_properties(yuzu PROPERTIES OUTPUT_NAME "eden")

2
src/yuzu/data_dialog.cpp

@ -95,7 +95,7 @@ void DataWidget::open()
user_id = selectProfile(); user_id = selectProfile();
} }
QDesktopServices::openUrl(QUrl::fromLocalFile( QDesktopServices::openUrl(QUrl::fromLocalFile(
QString::fromStdString(FrontendCommon::DataManager::GetDataDir(m_dir, user_id))));
QString::fromStdString(FrontendCommon::DataManager::GetDataDirString(m_dir, user_id))));
} }
void DataWidget::upload() void DataWidget::upload()

5
src/yuzu/game_list.cpp

@ -542,6 +542,7 @@ void GameList::PopupContextMenu(const QPoint& menu_location) {
} }
void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::string& path) { void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::string& path) {
// TODO(crueter): Refactor this and make it less bad
QAction* favorite = context_menu.addAction(tr("Favorite")); QAction* favorite = context_menu.addAction(tr("Favorite"));
context_menu.addSeparator(); context_menu.addSeparator();
QAction* start_game = context_menu.addAction(tr("Start Game")); QAction* start_game = context_menu.addAction(tr("Start Game"));
@ -581,6 +582,7 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
#endif #endif
context_menu.addSeparator(); context_menu.addSeparator();
QAction* properties = context_menu.addAction(tr("Configure Game")); QAction* properties = context_menu.addAction(tr("Configure Game"));
QAction* ryujinx = context_menu.addAction(tr("Link to Ryujinx"));
favorite->setVisible(program_id != 0); favorite->setVisible(program_id != 0);
favorite->setCheckable(true); favorite->setCheckable(true);
@ -662,6 +664,9 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
#endif #endif
connect(properties, &QAction::triggered, connect(properties, &QAction::triggered,
[this, path]() { emit OpenPerGameGeneralRequested(path); }); [this, path]() { emit OpenPerGameGeneralRequested(path); });
connect(ryujinx, &QAction::triggered, [this, program_id]() { emit LinkToRyujinxRequested(program_id);
});
}; };
void GameList::AddCustomDirPopup(QMenu& context_menu, QModelIndex selected) { void GameList::AddCustomDirPopup(QMenu& context_menu, QModelIndex selected) {

1
src/yuzu/game_list.h

@ -113,6 +113,7 @@ signals:
void NavigateToGamedbEntryRequested(u64 program_id, void NavigateToGamedbEntryRequested(u64 program_id,
const CompatibilityList& compatibility_list); const CompatibilityList& compatibility_list);
void OpenPerGameGeneralRequested(const std::string& file); void OpenPerGameGeneralRequested(const std::string& file);
void LinkToRyujinxRequested(const u64 &program_id);
void OpenDirectory(const QString& directory); void OpenDirectory(const QString& directory);
void AddDirectory(); void AddDirectory();
void ShowList(bool show); void ShowList(bool show);

61
src/yuzu/main.cpp

@ -6,10 +6,12 @@
#include "core/tools/renderdoc.h" #include "core/tools/renderdoc.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"
#include "qt_common/abstract/frontend.h"
#include "qt_common/util/content.h" #include "qt_common/util/content.h"
#include "qt_common/util/game.h" #include "qt_common/util/game.h"
#include "qt_common/util/meta.h" #include "qt_common/util/meta.h"
#include "qt_common/util/path.h" #include "qt_common/util/path.h"
#include "qt_common/util/fs.h"
#include <clocale> #include <clocale>
#include <cmath> #include <cmath>
#include <memory> #include <memory>
@ -108,6 +110,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
#include "common/detached_tasks.h" #include "common/detached_tasks.h"
#include "common/fs/fs.h" #include "common/fs/fs.h"
#include "common/fs/path_util.h" #include "common/fs/path_util.h"
#include "common/fs/ryujinx_compat.h"
#include "common/literals.h" #include "common/literals.h"
#include "common/logging/backend.h" #include "common/logging/backend.h"
#include "common/logging/log.h" #include "common/logging/log.h"
@ -160,6 +163,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
#include "yuzu/debugger/wait_tree.h" #include "yuzu/debugger/wait_tree.h"
#include "yuzu/data_dialog.h" #include "yuzu/data_dialog.h"
#include "yuzu/deps_dialog.h" #include "yuzu/deps_dialog.h"
#include "yuzu/ryujinx_dialog.h"
#include "qt_common/discord/discord.h" #include "qt_common/discord/discord.h"
#include "yuzu/game_list.h" #include "yuzu/game_list.h"
#include "yuzu/game_list_p.h" #include "yuzu/game_list_p.h"
@ -1597,6 +1601,8 @@ void GMainWindow::ConnectWidgetEvents() {
connect(game_list, &GameList::OpenPerGameGeneralRequested, this, connect(game_list, &GameList::OpenPerGameGeneralRequested, this,
&GMainWindow::OnGameListOpenPerGameProperties); &GMainWindow::OnGameListOpenPerGameProperties);
connect(game_list, &GameList::LinkToRyujinxRequested, this,
&GMainWindow::OnLinkToRyujinx);
connect(this, &GMainWindow::UpdateInstallProgress, this, connect(this, &GMainWindow::UpdateInstallProgress, this,
&GMainWindow::IncrementInstallProgress); &GMainWindow::IncrementInstallProgress);
@ -2875,6 +2881,61 @@ void GMainWindow::OnGameListOpenPerGameProperties(const std::string& file) {
OpenPerGameConfiguration(title_id, file); OpenPerGameConfiguration(title_id, file);
} }
std::string GMainWindow::GetProfileID()
{
const auto select_profile = [this] {
const Core::Frontend::ProfileSelectParameters parameters{
.mode = Service::AM::Frontend::UiMode::UserSelector,
.invalid_uid_list = {},
.display_options = {},
.purpose = Service::AM::Frontend::UserSelectionPurpose::General,
};
QtProfileSelectionDialog dialog(*QtCommon::system, this, parameters);
dialog.setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint
| Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint);
dialog.setWindowModality(Qt::WindowModal);
if (dialog.exec() == QDialog::Rejected) {
return -1;
}
return dialog.GetIndex();
};
const auto index = select_profile();
if (index == -1) {
return "";
}
const auto uuid = QtCommon::system->GetProfileManager().GetUser(static_cast<std::size_t>(index));
ASSERT(uuid);
const auto user_id = uuid->AsU128();
return fmt::format("{:016X}{:016X}", user_id[1], user_id[0]);
}
void GMainWindow::OnLinkToRyujinx(const u64& program_id)
{
u64 save_id = QtCommon::FS::GetRyujinxSaveID(program_id);
if (save_id == (u64) -1)
return;
const std::string user_id = GetProfileID();
auto paths = QtCommon::FS::GetEmuPaths(program_id, save_id, user_id);
if (!paths)
return;
auto eden_dir = paths.value().first;
auto ryu_dir = paths.value().second;
if (!QtCommon::FS::CheckUnlink(eden_dir, ryu_dir)) {
RyujinxDialog dialog(eden_dir, ryu_dir, this);
dialog.exec();
}
}
void GMainWindow::OnMenuLoadFile() { void GMainWindow::OnMenuLoadFile() {
if (is_load_file_select_active) { if (is_load_file_select_active) {
return; return;

3
src/yuzu/main.h

@ -358,6 +358,7 @@ private slots:
void OnGameListAddDirectory(); void OnGameListAddDirectory();
void OnGameListShowList(bool show); void OnGameListShowList(bool show);
void OnGameListOpenPerGameProperties(const std::string& file); void OnGameListOpenPerGameProperties(const std::string& file);
void OnLinkToRyujinx(const u64& program_id);
void OnMenuLoadFile(); void OnMenuLoadFile();
void OnMenuLoadFolder(); void OnMenuLoadFolder();
void IncrementInstallProgress(); void IncrementInstallProgress();
@ -470,6 +471,8 @@ private:
QMessageBox::StandardButtons(QMessageBox::Yes | QMessageBox::No), QMessageBox::StandardButtons(QMessageBox::Yes | QMessageBox::No),
QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); QMessageBox::StandardButton defaultButton = QMessageBox::NoButton);
std::string GetProfileID();
std::unique_ptr<Ui::MainWindow> ui; std::unique_ptr<Ui::MainWindow> ui;
std::unique_ptr<DiscordRPC::DiscordInterface> discord_rpc; std::unique_ptr<DiscordRPC::DiscordInterface> discord_rpc;

9
src/yuzu/migration_worker.cpp

@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
#include "migration_worker.h" #include "migration_worker.h"
#include "common/fs/symlink.h"
#include <QMap> #include <QMap>
#include <boost/algorithm/string/predicate.hpp> #include <boost/algorithm/string/predicate.hpp>
@ -37,7 +38,7 @@ void MigrationWorker::process()
try { try {
fs::remove_all(eden_dir); fs::remove_all(eden_dir);
} catch (fs::filesystem_error &_) { } catch (fs::filesystem_error &_) {
// ignore because linux does stupid crap sometimes.
// ignore because linux does stupid crap sometimes
} }
switch (strategy) { switch (strategy) {
@ -46,7 +47,7 @@ void MigrationWorker::process()
// Windows 11 has random permission nonsense to deal with. // Windows 11 has random permission nonsense to deal with.
try { try {
fs::create_directory_symlink(legacy_user_dir, eden_dir);
Common::FS::CreateSymlink(legacy_user_dir, eden_dir);
} catch (const fs::filesystem_error &e) { } catch (const fs::filesystem_error &e) {
emit error(tr("Linking the old directory failed. You may need to re-run with " emit error(tr("Linking the old directory failed. You may need to re-run with "
"administrative privileges on Windows.\nOS gave error: %1") "administrative privileges on Windows.\nOS gave error: %1")
@ -58,11 +59,11 @@ void MigrationWorker::process()
// are already children of the root directory // are already children of the root directory
#ifndef WIN32 #ifndef WIN32
if (fs::is_directory(legacy_config_dir)) { if (fs::is_directory(legacy_config_dir)) {
fs::create_directory_symlink(legacy_config_dir, config_dir);
Common::FS::CreateSymlink(legacy_config_dir, config_dir);
} }
if (fs::is_directory(legacy_cache_dir)) { if (fs::is_directory(legacy_cache_dir)) {
fs::create_directory_symlink(legacy_cache_dir, cache_dir);
Common::FS::CreateSymlink(legacy_cache_dir, cache_dir);
} }
#endif #endif

6
src/yuzu/migration_worker.h

@ -12,9 +12,9 @@ using namespace Common::FS;
typedef struct Emulator { typedef struct Emulator {
const char *m_name; const char *m_name;
LegacyPath e_user_dir;
LegacyPath e_config_dir;
LegacyPath e_cache_dir;
EmuPath e_user_dir;
EmuPath e_config_dir;
EmuPath e_cache_dir;
const std::string get_user_dir() const { const std::string get_user_dir() const {
return Common::FS::GetLegacyPath(e_user_dir).string(); return Common::FS::GetLegacyPath(e_user_dir).string();

40
src/yuzu/ryujinx_dialog.cpp

@ -0,0 +1,40 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include "ryujinx_dialog.h"
#include "qt_common/util/fs.h"
#include "ui_ryujinx_dialog.h"
#include <filesystem>
namespace fs = std::filesystem;
RyujinxDialog::RyujinxDialog(std::filesystem::path eden_path,
std::filesystem::path ryu_path,
QWidget *parent)
: QDialog(parent)
, ui(new Ui::RyujinxDialog)
, m_eden(eden_path.make_preferred())
, m_ryu(ryu_path.make_preferred())
{
ui->setupUi(this);
connect(ui->eden, &QPushButton::clicked, this, &RyujinxDialog::fromEden);
connect(ui->ryujinx, &QPushButton::clicked, this, &RyujinxDialog::fromRyujinx);
}
RyujinxDialog::~RyujinxDialog()
{
delete ui;
}
void RyujinxDialog::fromEden()
{
accept();
QtCommon::FS::LinkRyujinx(m_eden, m_ryu);
}
void RyujinxDialog::fromRyujinx()
{
accept();
QtCommon::FS::LinkRyujinx(m_ryu, m_eden);
}

32
src/yuzu/ryujinx_dialog.h

@ -0,0 +1,32 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#ifndef RYUJINX_DIALOG_H
#define RYUJINX_DIALOG_H
#include <QDialog>
#include <filesystem>
namespace Ui {
class RyujinxDialog;
}
class RyujinxDialog : public QDialog
{
Q_OBJECT
public:
explicit RyujinxDialog(std::filesystem::path eden_path, std::filesystem::path ryu_path, QWidget *parent = nullptr);
~RyujinxDialog();
private slots:
void fromEden();
void fromRyujinx();
private:
Ui::RyujinxDialog *ui;
std::filesystem::path m_eden;
std::filesystem::path m_ryu;
};
#endif // RYUJINX_DIALOG_H

81
src/yuzu/ryujinx_dialog.ui

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>RyujinxDialog</class>
<widget class="QDialog" name="RyujinxDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>404</width>
<height>170</height>
</rect>
</property>
<property name="windowTitle">
<string>Ryujinx Link</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Linking save data to Ryujinx lets both Ryujinx and Eden reference the same save files for your games.
By selecting &quot;From Eden&quot;, previous save data stored in Ryujinx will be deleted, and vice versa for &quot;From Ryujinx&quot;.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="eden">
<property name="text">
<string>From Eden</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="ryujinx">
<property name="text">
<string>From Ryujinx</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cancel">
<property name="text">
<string>Cancel</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>cancel</sender>
<signal>clicked()</signal>
<receiver>RyujinxDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>331</x>
<y>147</y>
</hint>
<hint type="destinationlabel">
<x>201</x>
<y>84</y>
</hint>
</hints>
</connection>
</connections>
</ui>
Loading…
Cancel
Save