From f1e3e8d2db0a4e6dd404bf5e75d4ed84541e5603 Mon Sep 17 00:00:00 2001 From: crueter Date: Wed, 22 Oct 2025 16:23:58 -0400 Subject: [PATCH] WIP: [qt] Ryujinx save data link This adds an action to the Game List context menu that lets users link save data from Eden to Ryujinx, or vice versa. I'll document this more later, but basically the gist of it is: - read title_id -> save_id pairs from imkvdb.arc - find a match in ryujinx - if it exists, symlink it This needs extensive testing on Windows. I have no idea if `mklink /J` will work how I want it to. But it is confirmed working on Linux (minus one of my drives being exfat which is... mildly annoying) Signed-off-by: crueter --- src/common/CMakeLists.txt | 1 + src/common/fs/fs_paths.h | 1 + src/common/fs/path_util.cpp | 28 +++++--- src/common/fs/path_util.h | 18 ++++-- src/common/ryujinx_compat.cpp | 97 ++++++++++++++++++++++++++++ src/common/ryujinx_compat.h | 40 ++++++++++++ src/frontend_common/data_manager.cpp | 12 ++-- src/frontend_common/data_manager.h | 4 +- src/qt_common/qt_common.cpp | 36 ++++++++++- src/qt_common/qt_common.h | 2 + src/qt_common/qt_string_lookup.h | 23 ++++++- src/qt_common/util/content.cpp | 4 +- src/qt_common/util/content.h | 6 +- src/yuzu/CMakeLists.txt | 1 + src/yuzu/data_dialog.cpp | 2 +- src/yuzu/game_list.cpp | 5 ++ src/yuzu/game_list.h | 1 + src/yuzu/main.cpp | 55 ++++++++++++++++ src/yuzu/main.h | 3 + src/yuzu/migration_worker.h | 6 +- src/yuzu/ryujinx_dialog.cpp | 65 +++++++++++++++++++ src/yuzu/ryujinx_dialog.h | 34 ++++++++++ src/yuzu/ryujinx_dialog.ui | 81 +++++++++++++++++++++++ 23 files changed, 493 insertions(+), 32 deletions(-) create mode 100644 src/common/ryujinx_compat.cpp create mode 100644 src/common/ryujinx_compat.h create mode 100644 src/yuzu/ryujinx_dialog.cpp create mode 100644 src/yuzu/ryujinx_dialog.h create mode 100644 src/yuzu/ryujinx_dialog.ui diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index 21f2ab4ed4..9ad58af1c5 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -155,6 +155,7 @@ add_library( wall_clock.h zstd_compression.cpp zstd_compression.h + ryujinx_compat.h ryujinx_compat.cpp ) if(WIN32) diff --git a/src/common/fs/fs_paths.h b/src/common/fs/fs_paths.h index 5cdf9be39d..640a83c44b 100644 --- a/src/common/fs/fs_paths.h +++ b/src/common/fs/fs_paths.h @@ -33,6 +33,7 @@ #define SUDACHI_DIR "sudachi" #define YUZU_DIR "yuzu" #define SUYU_DIR "suyu" +#define RYUJINX_DIR "Ryujinx" // yuzu-specific files #define LOG_FILE "eden_log.txt" diff --git a/src/common/fs/path_util.cpp b/src/common/fs/path_util.cpp index a095e0c239..4bbb3ccbd3 100644 --- a/src/common/fs/path_util.cpp +++ b/src/common/fs/path_util.cpp @@ -84,7 +84,7 @@ public: 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); } @@ -98,7 +98,7 @@ public: 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); } @@ -140,9 +140,9 @@ public: eden_path_cache = eden_path / CACHE_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(Sudachi, SUDACHI) LEGACY_PATH(Yuzu, YUZU) @@ -165,6 +165,16 @@ public: GenerateEdenPath(EdenPath::ShaderDir, eden_path / SHADER_DIR); GenerateEdenPath(EdenPath::TASDir, eden_path / TAS_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 alas, + GenerateLegacyPath(EmuPath::RyujinxDir, GetDataDirectory("XDG_CONFIG_HOME") / RYUJINX_DIR); +#endif + } private: @@ -179,12 +189,12 @@ private: 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); } std::unordered_map eden_paths; - std::unordered_map legacy_paths; + std::unordered_map legacy_paths; }; bool ValidatePath(const fs::path& path) { @@ -272,7 +282,7 @@ const fs::path& GetEdenPath(EdenPath 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); } @@ -280,7 +290,7 @@ std::string GetEdenPathString(EdenPath eden_path) { return PathToUTF8String(GetEdenPath(eden_path)); } -std::string GetLegacyPathString(LegacyPath legacy_path) { +std::string GetLegacyPathString(EmuPath legacy_path) { return PathToUTF8String(GetLegacyPath(legacy_path)); } diff --git a/src/common/fs/path_util.h b/src/common/fs/path_util.h index b34efc472f..dc800b2892 100644 --- a/src/common/fs/path_util.h +++ b/src/common/fs/path_util.h @@ -32,22 +32,26 @@ enum class EdenPath { IconsDir, // Where Icons for Windows shortcuts are stored. }; -enum LegacyPath { - CitronDir, // Citron Directories for migration +// migration/compat dirs +enum EmuPath { + CitronDir, CitronConfigDir, CitronCacheDir, - SudachiDir, // Sudachi Directories for migration + SudachiDir, SudachiConfigDir, SudachiCacheDir, - YuzuDir, // Yuzu Directories for migration + YuzuDir, YuzuConfigDir, YuzuCacheDir, - SuyuDir, // Suyu Directories for migration + SuyuDir, SuyuConfigDir, 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. */ -[[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. @@ -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. */ -[[nodiscard]] std::string GetLegacyPathString(LegacyPath legacy_path); +[[nodiscard]] std::string GetLegacyPathString(EmuPath legacy_path); /** * Sets a new filesystem path associated with the EdenPath enum. diff --git a/src/common/ryujinx_compat.cpp b/src/common/ryujinx_compat.cpp new file mode 100644 index 0000000000..ce5c9699cd --- /dev/null +++ b/src/common/ryujinx_compat.cpp @@ -0,0 +1,97 @@ +#include "ryujinx_compat.h" +#include "common/fs/path_util.h" +#include +#include +#include +#include +#include + +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 &imens) +{ + std::ifstream kvdb{path, std::ios::binary | std::ios::ate}; + + // TODO: error codes + 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.resize(num_imens / 2); + + // 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; + + // I have no idea why this is but we can basically just... ignore every other IMEN + if (i % 2 == 0) { + kvdb.ignore(IMEN_SIZE); + continue; + } + + kvdb.read(magic, 4); + if (std::memcmp(magic, IMEN_MAGIC, 4) != 0) { + return IMENReadResult::InvalidMagic; + } + + kvdb.ignore(0x8); + kvdb.read(reinterpret_cast(&title_id), 8); + kvdb.ignore(0x38); + kvdb.read(reinterpret_cast(&save_id), 8); + kvdb.ignore(0x38); + + imens[i / 2] = IMEN{ title_id, save_id}; + } + + return IMENReadResult::Success; +} +} diff --git a/src/common/ryujinx_compat.h b/src/common/ryujinx_compat.h new file mode 100644 index 0000000000..6273754074 --- /dev/null +++ b/src/common/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 +#include + +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 &imens); + +} // namespace Common::FS diff --git a/src/frontend_common/data_manager.cpp b/src/frontend_common/data_manager.cpp index 83eccbde17..1df828d028 100644 --- a/src/frontend_common/data_manager.cpp +++ b/src/frontend_common/data_manager.cpp @@ -4,14 +4,13 @@ #include "data_manager.h" #include "common/assert.h" #include "common/fs/path_util.h" -#include #include namespace FrontendCommon::DataManager { 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); @@ -35,6 +34,11 @@ const std::string GetDataDir(DataDir dir, const std::string &user_id) 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) { fs::path data_dir = GetDataDir(dir, user_id); @@ -65,7 +69,7 @@ u64 DataDirSize(DataDir dir) if (!fs::exists(data_dir)) 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()) { size += entry.file_size(); } @@ -74,4 +78,4 @@ u64 DataDirSize(DataDir dir) return size; } -} +} // namespace FrontendCommon::DataManager diff --git a/src/frontend_common/data_manager.h b/src/frontend_common/data_manager.h index d67bd20467..c075ab54b6 100644 --- a/src/frontend_common/data_manager.h +++ b/src/frontend_common/data_manager.h @@ -6,12 +6,14 @@ #include "common/common_types.h" #include +#include namespace FrontendCommon::DataManager { 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 = ""); diff --git a/src/qt_common/qt_common.cpp b/src/qt_common/qt_common.cpp index 1fa3df98cf..a056cfe9ed 100644 --- a/src/qt_common/qt_common.cpp +++ b/src/qt_common/qt_common.cpp @@ -3,11 +3,14 @@ #include "qt_common.h" #include "common/fs/fs.h" +#include "common/ryujinx_compat.h" #include #include #include "common/logging/log.h" #include "core/frontend/emu_window.h" +#include "qt_common/abstract/qt_frontend_util.h" +#include "qt_common/qt_string_lookup.h" #include @@ -33,7 +36,8 @@ std::unique_ptr system = nullptr; std::shared_ptr vfs = nullptr; std::unique_ptr provider = nullptr; -Core::Frontend::WindowSystemType GetWindowSystemType() { +Core::Frontend::WindowSystemType GetWindowSystemType() +{ // Determine WSI type based on Qt platform. QString platform_name = QGuiApplication::platformName(); if (platform_name == QStringLiteral("windows")) @@ -101,9 +105,11 @@ void Init(QObject* root) provider = std::make_unique(); } -std::filesystem::path GetEdenCommand() { +std::filesystem::path GetEdenCommand() +{ std::filesystem::path command; + // TODO: flatpak? QString appimage = QString::fromLocal8Bit(getenv("APPIMAGE")); if (!appimage.isEmpty()) { command = std::filesystem::path{appimage.toStdString()}; @@ -120,4 +126,30 @@ std::filesystem::path GetEdenCommand() { return command; } +u64 GetRyujinxSaveID(const u64& program_id) +{ + auto path = Common::FS::GetKvdbPath(); + std::vector 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((int) res + (int) StringLookup::KvdbNonexistent)); + QtCommon::Frontend::Critical(tr("Could not find Ryujinx save data"), caption); + } + + return -1; +} + } // namespace QtCommon diff --git a/src/qt_common/qt_common.h b/src/qt_common/qt_common.h index a2700427ab..5a936873e1 100644 --- a/src/qt_common/qt_common.h +++ b/src/qt_common/qt_common.h @@ -24,6 +24,8 @@ extern std::unique_ptr system; extern std::shared_ptr vfs; extern std::unique_ptr provider; +u64 GetRyujinxSaveID(const u64 &program_id); + typedef std::function QtProgressCallback; Core::Frontend::WindowSystemType GetWindowSystemType(); diff --git a/src/qt_common/qt_string_lookup.h b/src/qt_common/qt_string_lookup.h index 7756bcdaf2..42d71e6f9d 100644 --- a/src/qt_common/qt_string_lookup.h +++ b/src/qt_common/qt_string_lookup.h @@ -42,9 +42,18 @@ enum StringKey { MigrationTooltipClearOld, MigrationTooltipLinkOld, + // ryujinx + KvdbNonexistent, + KvdbNoHeader, + KvdbInvalidMagic, + KvdbMisaligned, + KvdbNoImens, + RyujinxNoSaveId, + }; -static const frozen::map strings = { +static const constexpr frozen::map strings = { + // 0-4 {SavesTooltip, QT_TR_NOOP("Contains game save data. DO NOT REMOVE UNLESS YOU KNOW WHAT YOU'RE DOING!")}, {ShadersTooltip, @@ -54,6 +63,7 @@ static const frozen::map strings = { {ModsTooltip, QT_TR_NOOP("Contains game mods, patches, and cheats.")}, // Key install + // 5-9 {KeyInstallSuccess, QT_TR_NOOP("Decryption Keys were successfully installed")}, {KeyInstallInvalidDir, QT_TR_NOOP("Unable to read key directory, aborting")}, {KeyInstallErrorFailedCopy, QT_TR_NOOP("One or more keys failed to copy.")}, @@ -65,6 +75,7 @@ static const frozen::map strings = { "re-dump keys.")}, // fw install + // 10-14 {FwInstallSuccess, QT_TR_NOOP("Successfully installed firmware version %1")}, {FwInstallNoNCAs, QT_TR_NOOP("Unable to locate potential firmware NCA files")}, {FwInstallFailedDelete, QT_TR_NOOP("Failed to delete one or more firmware files.")}, @@ -75,6 +86,7 @@ static const frozen::map strings = { "Eden or re-install firmware.")}, // migrator + // 15-20 {MigrationPromptPrefix, QT_TR_NOOP("Eden has detected user data for the following emulators:")}, {MigrationPrompt, QT_TR_NOOP("Would you like to migrate your data for use in Eden?\n" @@ -93,6 +105,15 @@ static const frozen::map strings = { {MigrationTooltipLinkOld, 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.")}, + + // 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) diff --git a/src/qt_common/util/content.cpp b/src/qt_common/util/content.cpp index f7487ac36f..a1bd9b941e 100644 --- a/src/qt_common/util/content.cpp +++ b/src/qt_common/util/content.cpp @@ -391,7 +391,7 @@ void ExportDataDir(FrontendCommon::DataManager::DataDir data_dir, std::function callback) { 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"), tr("%1.zip").arg(name), @@ -455,7 +455,7 @@ void ImportDataDir(FrontendCommon::DataManager::DataDir data_dir, const std::string& user_id, std::function 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; diff --git a/src/qt_common/util/content.h b/src/qt_common/util/content.h index d1384546e1..4102737fb1 100644 --- a/src/qt_common/util/content.h +++ b/src/qt_common/util/content.h @@ -25,7 +25,8 @@ enum class FirmwareInstallResult { inline const QString GetFirmwareInstallResultString(FirmwareInstallResult result) { - return QtCommon::StringLookup::Lookup(static_cast((int) result + (int) QtCommon::StringLookup::FwInstallSuccess)); + return QtCommon::StringLookup::Lookup(static_cast( + (int) result + (int) QtCommon::StringLookup::FwInstallSuccess)); } /** @@ -36,7 +37,8 @@ inline const QString GetFirmwareInstallResultString(FirmwareInstallResult result inline const QString GetKeyInstallResultString(FirmwareManager::KeyInstallResult result) { // this can probably be made into a common function of sorts - return QtCommon::StringLookup::Lookup(static_cast((int) result + (int) QtCommon::StringLookup::KeyInstallSuccess)); + return QtCommon::StringLookup::Lookup(static_cast( + (int) result + (int) QtCommon::StringLookup::KeyInstallSuccess)); } void InstallFirmware(const QString &location, bool recursive); diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index 0bb51b250e..165ced55e8 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt @@ -237,6 +237,7 @@ add_executable(yuzu data_dialog.h data_dialog.cpp data_dialog.ui data_widget.ui + ryujinx_dialog.h ryujinx_dialog.cpp ryujinx_dialog.ui ) set_target_properties(yuzu PROPERTIES OUTPUT_NAME "eden") diff --git a/src/yuzu/data_dialog.cpp b/src/yuzu/data_dialog.cpp index dc3e06f89f..008e810eae 100644 --- a/src/yuzu/data_dialog.cpp +++ b/src/yuzu/data_dialog.cpp @@ -95,7 +95,7 @@ void DataWidget::open() user_id = selectProfile(); } 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() diff --git a/src/yuzu/game_list.cpp b/src/yuzu/game_list.cpp index 875e794181..ae8e4688b4 100644 --- a/src/yuzu/game_list.cpp +++ b/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) { + // TODO(crueter): Refactor this and make it less bad QAction* favorite = context_menu.addAction(tr("Favorite")); context_menu.addSeparator(); 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 context_menu.addSeparator(); QAction* properties = context_menu.addAction(tr("Configure Game")); + QAction* ryujinx = context_menu.addAction(tr("Link to Ryujinx")); favorite->setVisible(program_id != 0); favorite->setCheckable(true); @@ -662,6 +664,9 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri #endif connect(properties, &QAction::triggered, [this, path]() { emit OpenPerGameGeneralRequested(path); }); + + connect(ryujinx, &QAction::triggered, [this, program_id]() { emit LinkToRyujinxRequested(program_id); + }); }; void GameList::AddCustomDirPopup(QMenu& context_menu, QModelIndex selected) { diff --git a/src/yuzu/game_list.h b/src/yuzu/game_list.h index b4cdf61f0d..0b3ac7fcde 100644 --- a/src/yuzu/game_list.h +++ b/src/yuzu/game_list.h @@ -113,6 +113,7 @@ signals: void NavigateToGamedbEntryRequested(u64 program_id, const CompatibilityList& compatibility_list); void OpenPerGameGeneralRequested(const std::string& file); + void LinkToRyujinxRequested(const u64 &program_id); void OpenDirectory(const QString& directory); void AddDirectory(); void ShowList(bool show); diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index 8fb5575b41..fb4b4e109a 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -6,6 +6,7 @@ #include "core/tools/renderdoc.h" #include "frontend_common/firmware_manager.h" #include "qt_common/qt_common.h" +#include "qt_common/abstract/qt_frontend_util.h" #include "qt_common/util/content.h" #include "qt_common/util/game.h" #include "qt_common/util/meta.h" @@ -108,6 +109,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual #include "common/detached_tasks.h" #include "common/fs/fs.h" #include "common/fs/path_util.h" +#include "common/ryujinx_compat.h" #include "common/literals.h" #include "common/logging/backend.h" #include "common/logging/log.h" @@ -160,6 +162,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual #include "yuzu/debugger/wait_tree.h" #include "yuzu/data_dialog.h" #include "yuzu/deps_dialog.h" +#include "yuzu/ryujinx_dialog.h" #include "qt_common/discord/discord.h" #include "yuzu/game_list.h" #include "yuzu/game_list_p.h" @@ -1597,6 +1600,8 @@ void GMainWindow::ConnectWidgetEvents() { connect(game_list, &GameList::OpenPerGameGeneralRequested, this, &GMainWindow::OnGameListOpenPerGameProperties); + connect(game_list, &GameList::LinkToRyujinxRequested, this, + &GMainWindow::OnLinkToRyujinx); connect(this, &GMainWindow::UpdateInstallProgress, this, &GMainWindow::IncrementInstallProgress); @@ -2875,6 +2880,56 @@ void GMainWindow::OnGameListOpenPerGameProperties(const std::string& 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(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) +{ + namespace fs = std::filesystem; + + u64 save_id = QtCommon::GetRyujinxSaveID(program_id); + fs::path ryu_dir = Common::FS::GetRyuSavePath(save_id); + + std::string user_id = GetProfileID(); + std::string hex_program = fmt::format("{:016X}", program_id); + fs::path eden_dir = FrontendCommon::DataManager::GetDataDir(FrontendCommon::DataManager::DataDir::Saves) + / user_id / hex_program; + + RyujinxDialog dialog(eden_dir, ryu_dir, this); + dialog.exec(); +} + void GMainWindow::OnMenuLoadFile() { if (is_load_file_select_active) { return; diff --git a/src/yuzu/main.h b/src/yuzu/main.h index ce04d6d152..ae5d74215a 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h @@ -358,6 +358,7 @@ private slots: void OnGameListAddDirectory(); void OnGameListShowList(bool show); void OnGameListOpenPerGameProperties(const std::string& file); + void OnLinkToRyujinx(const u64& program_id); void OnMenuLoadFile(); void OnMenuLoadFolder(); void IncrementInstallProgress(); @@ -470,6 +471,8 @@ private: QMessageBox::StandardButtons(QMessageBox::Yes | QMessageBox::No), QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); + std::string GetProfileID(); + std::unique_ptr ui; std::unique_ptr discord_rpc; diff --git a/src/yuzu/migration_worker.h b/src/yuzu/migration_worker.h index 5f9e400db0..2ff6e8c481 100644 --- a/src/yuzu/migration_worker.h +++ b/src/yuzu/migration_worker.h @@ -12,9 +12,9 @@ using namespace Common::FS; typedef struct Emulator { 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 { return Common::FS::GetLegacyPath(e_user_dir).string(); diff --git a/src/yuzu/ryujinx_dialog.cpp b/src/yuzu/ryujinx_dialog.cpp new file mode 100644 index 0000000000..56867f715f --- /dev/null +++ b/src/yuzu/ryujinx_dialog.cpp @@ -0,0 +1,65 @@ +#include "qt_common/abstract/qt_frontend_util.h" +#include "ryujinx_dialog.h" +#include "ui_ryujinx_dialog.h" +#include +#include +#include + +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) + , m_ryu(ryu_path) +{ + 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(); + link(m_eden, m_ryu); +} + +void RyujinxDialog::fromRyujinx() +{ + accept(); + link(m_ryu, m_eden); +} + +void RyujinxDialog::link(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); + +#ifdef _WIN32 + const std::string command = fmt::format("mklink /J {} {}", to.string(), from.string()); + system(command.c_str()); +#else + try { + fs::create_directory_symlink(from, to); + } catch (std::exception &e) { + QtCommon::Frontend::Critical(tr("Failed to link save data"), + tr("Could not link directory:\n\t%1\nTo:\n\t%2\n\nError: %3") + .arg(QString::fromStdString(from.string()), + QString::fromStdString(to.string()), + QString::fromStdString(e.what()))); + return; + } +#endif + + QtCommon::Frontend::Information(tr("Linked Save Data"), tr("Save data has been linked.")); +} diff --git a/src/yuzu/ryujinx_dialog.h b/src/yuzu/ryujinx_dialog.h new file mode 100644 index 0000000000..920c813e43 --- /dev/null +++ b/src/yuzu/ryujinx_dialog.h @@ -0,0 +1,34 @@ +#ifndef RYUJINX_DIALOG_H +#define RYUJINX_DIALOG_H + +#include +#include + +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; + + /// @brief Link two directories + /// @param from The symlink target + /// @param to The symlink name (will be deleted) + void link(std::filesystem::path &from, std::filesystem::path &to); +}; + +#endif // RYUJINX_DIALOG_H diff --git a/src/yuzu/ryujinx_dialog.ui b/src/yuzu/ryujinx_dialog.ui new file mode 100644 index 0000000000..c4fb5ae51f --- /dev/null +++ b/src/yuzu/ryujinx_dialog.ui @@ -0,0 +1,81 @@ + + + RyujinxDialog + + + + 0 + 0 + 404 + 170 + + + + Ryujinx Link + + + + + + + 0 + 0 + + + + Linking save data to Ryujinx lets both Ryujinx and Eden reference the same save files for your games. + +By selecting "From Eden", previous save data stored in Ryujinx will be deleted, and vice versa for "From Ryujinx". + + + true + + + + + + + + + From Eden + + + + + + + From Ryujinx + + + + + + + Cancel + + + + + + + + + + + cancel + clicked() + RyujinxDialog + reject() + + + 331 + 147 + + + 201 + 84 + + + + +