diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index b6b2931fd9..2c2eeb189c 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -153,6 +153,8 @@ add_library( wall_clock.h zstd_compression.cpp zstd_compression.h + fs/ryujinx_compat.h fs/ryujinx_compat.cpp + fs/symlink.h fs/symlink.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..dc1071b9ca 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); } @@ -118,9 +118,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, 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(Sudachi, SUDACHI) LEGACY_PATH(Yuzu, YUZU) @@ -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,15 @@ 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 whatever + GenerateLegacyPath(EmuPath::RyujinxDir, GetDataDirectory("XDG_CONFIG_HOME") / RYUJINX_DIR); +#endif + } private: @@ -179,12 +188,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 +281,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 +289,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/fs/ryujinx_compat.cpp b/src/common/fs/ryujinx_compat.cpp new file mode 100644 index 0000000000..610ba736a7 --- /dev/null +++ b/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 +#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}; + + 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(&title_id), 8); + kvdb.ignore(0x38); + kvdb.read(reinterpret_cast(&save_id), 8); + kvdb.ignore(0x38); + + imens.emplace_back(IMEN{title_id, save_id}); + } + + return IMENReadResult::Success; +} + +} // namespace Common::FS diff --git a/src/common/fs/ryujinx_compat.h b/src/common/fs/ryujinx_compat.h new file mode 100644 index 0000000000..6273754074 --- /dev/null +++ b/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 +#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/common/fs/symlink.cpp b/src/common/fs/symlink.cpp new file mode 100644 index 0000000000..9c9fe3d50b --- /dev/null +++ b/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 +#include +#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 diff --git a/src/common/fs/symlink.h b/src/common/fs/symlink.h new file mode 100644 index 0000000000..3bfff95ebf --- /dev/null +++ b/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 +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 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/CMakeLists.txt b/src/qt_common/CMakeLists.txt index 39dd1a7460..75ef717583 100644 --- a/src/qt_common/CMakeLists.txt +++ b/src/qt_common/CMakeLists.txt @@ -20,13 +20,14 @@ add_library(qt_common STATIC util/applet.h util/applet.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 qt_string_lookup.h qt_compat.h discord/discord.h + util/fs.h util/fs.cpp ) create_target_directory_groups(qt_common) diff --git a/src/qt_common/abstract/qt_frontend_util.cpp b/src/qt_common/abstract/frontend.cpp similarity index 98% rename from src/qt_common/abstract/qt_frontend_util.cpp rename to src/qt_common/abstract/frontend.cpp index 3fe0ba0a80..2ed76f8ea9 100644 --- a/src/qt_common/abstract/qt_frontend_util.cpp +++ b/src/qt_common/abstract/frontend.cpp @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later -#include "qt_frontend_util.h" +#include "frontend.h" #include "qt_common/qt_common.h" #ifdef YUZU_QT_WIDGETS diff --git a/src/qt_common/abstract/qt_frontend_util.h b/src/qt_common/abstract/frontend.h similarity index 98% rename from src/qt_common/abstract/qt_frontend_util.h rename to src/qt_common/abstract/frontend.h index 59e8f15a4e..b496b37cbe 100644 --- a/src/qt_common/abstract/qt_frontend_util.h +++ b/src/qt_common/abstract/frontend.h @@ -1,8 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project // 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 #include "qt_common/qt_common.h" @@ -136,4 +136,4 @@ const QString GetSaveFileName(const QString &title, Options options = Options()); } // namespace QtCommon::Frontend -#endif // QT_FRONTEND_UTIL_H +#endif // FRONTEND_H diff --git a/src/qt_common/qt_common.cpp b/src/qt_common/qt_common.cpp index 1fa3df98cf..f2091df866 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/fs/ryujinx_compat.h" #include #include #include "common/logging/log.h" #include "core/frontend/emu_window.h" +#include "qt_common/abstract/frontend.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()}; 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 4360efeef0..df4435f567 100644 --- a/src/qt_common/util/content.cpp +++ b/src/qt_common/util/content.cpp @@ -11,7 +11,7 @@ #include "frontend_common/firmware_manager.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/qt_common.h" @@ -404,7 +404,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), @@ -468,7 +468,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/qt_common/util/fs.cpp b/src/qt_common/util/fs.cpp new file mode 100644 index 0000000000..da59a4e675 --- /dev/null +++ b/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 +#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 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; +} + +std::optional > GetEmuPaths( + const u64 program_id, const u64 save_id, const std::string &user_id) +{ + fs::path ryu_dir = Common::FS::GetRyuSavePath(save_id); + + if (user_id.empty()) + return std::nullopt; + + std::string hex_program = fmt::format("{:016X}", program_id); + fs::path eden_dir + = FrontendCommon::DataManager::GetDataDir(FrontendCommon::DataManager::DataDir::Saves, + user_id) + / hex_program; + + return std::make_pair(eden_dir, ryu_dir); +} + +} // namespace QtCommon::FS diff --git a/src/qt_common/util/fs.h b/src/qt_common/util/fs.h new file mode 100644 index 0000000000..ee8859a2e5 --- /dev/null +++ b/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 +#include + +#pragma once + +namespace QtCommon::FS { + +void LinkRyujinx(std::filesystem::path &from, std::filesystem::path &to); +u64 GetRyujinxSaveID(const u64 &program_id); + +/// @brief {eden, ryu} +std::optional> GetEmuPaths( + const u64 program_id, const u64 save_id, const std::string &user_id); + +/// 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 diff --git a/src/qt_common/util/game.cpp b/src/qt_common/util/game.cpp index e5018d24cb..e34a388993 100644 --- a/src/qt_common/util/game.cpp +++ b/src/qt_common/util/game.cpp @@ -8,7 +8,7 @@ #include "core/file_sys/savedata_factory.h" #include "core/hle/service/am/am_types.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/qt_common.h" #include "yuzu/util/util.h" diff --git a/src/qt_common/util/path.cpp b/src/qt_common/util/path.cpp index 93e8007eb9..73689058c6 100644 --- a/src/qt_common/util/path.cpp +++ b/src/qt_common/util/path.cpp @@ -7,7 +7,7 @@ #include #include "common/fs/fs.h" #include "common/fs/path_util.h" -#include "qt_common/abstract/qt_frontend_util.h" +#include "qt_common/abstract/frontend.h" #include namespace QtCommon::Path { diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index 4b1e87b6ed..d6082dd00b 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt @@ -236,6 +236,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..849ffa84de 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -6,10 +6,12 @@ #include "core/tools/renderdoc.h" #include "frontend_common/firmware_manager.h" #include "qt_common/qt_common.h" +#include "qt_common/abstract/frontend.h" #include "qt_common/util/content.h" #include "qt_common/util/game.h" #include "qt_common/util/meta.h" #include "qt_common/util/path.h" +#include "qt_common/util/fs.h" #include #include #include @@ -108,6 +110,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/fs/ryujinx_compat.h" #include "common/literals.h" #include "common/logging/backend.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/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 +1601,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 +2881,61 @@ 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) +{ + 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() { 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.cpp b/src/yuzu/migration_worker.cpp index b39bd22584..50b6d1ebd4 100644 --- a/src/yuzu/migration_worker.cpp +++ b/src/yuzu/migration_worker.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include "migration_worker.h" +#include "common/fs/symlink.h" #include #include @@ -37,7 +38,7 @@ void MigrationWorker::process() try { fs::remove_all(eden_dir); } catch (fs::filesystem_error &_) { - // ignore because linux does stupid crap sometimes. + // ignore because linux does stupid crap sometimes } switch (strategy) { @@ -46,7 +47,7 @@ void MigrationWorker::process() // Windows 11 has random permission nonsense to deal with. try { - fs::create_directory_symlink(legacy_user_dir, eden_dir); + Common::FS::CreateSymlink(legacy_user_dir, eden_dir); } catch (const fs::filesystem_error &e) { emit error(tr("Linking the old directory failed. You may need to re-run with " "administrative privileges on Windows.\nOS gave error: %1") @@ -58,11 +59,11 @@ void MigrationWorker::process() // are already children of the root directory #ifndef WIN32 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)) { - fs::create_directory_symlink(legacy_cache_dir, cache_dir); + Common::FS::CreateSymlink(legacy_cache_dir, cache_dir); } #endif 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..db10c06d93 --- /dev/null +++ b/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 + +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); +} diff --git a/src/yuzu/ryujinx_dialog.h b/src/yuzu/ryujinx_dialog.h new file mode 100644 index 0000000000..63cebe483c --- /dev/null +++ b/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 +#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; +}; + +#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 + + + + +