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 + + + + +