This adds an action to the Game List context menu that lets users link save data from Eden to Ryujinx, or vice versa. Unfortunately, this isn't so simple to deal with due to the way Ryujinx's saves work. Ryujinx stores its saves in the... config directory... in `bis/user/save`. Unlike Yuzu, however, it doesn't store things by TitleID, instead it's just a bunch of directories from 000...01 to 000...0f and so on. The way it *maps* TitleID to SaveID is via `imkvdb.arc` in `bis/system/save/8000000000000000/0/` and also an identical copy in the `1` directory for... some reason. `imkvdb.arc` is handled by `FlatMapKeyValueStore` in LibHac, which, as the name implies, is a key-value storage system that `imkvdb.arc`, and seemingly `imkvdb.arc` alone, uses. The way this class is written is really weird, almost as if it's designed to accommodate more types of kvdbs... but for now we can safely assume that there aren't gonna be any other `kvdb` implementations added to HorizonNX. Regardless, the file format is ridiculously simple so I didn't actually need to do a deep dive into C# code... of which I can basically only read Avalonia. A simple `xxd` on the `imkvdb.arc` is all that's needed, and here's everything that matters: - The `IMKV` magic header (4 bytes) - 8 bytes that don't really have anything useful to us, except for a size byte (presumably a `u32`) strewn at offset `0x08` from the start of the file, which is useless to us - Then we start the `IMEN` list. I don't know what the `IM` stands for, but `IMEN` is just, well, an ENtry. Offsets shown are relative to the start of the `IMEN` header. * 4-byte `IMEN` magic header at 0x0 * 8 bytes of filler data. It contains two `0x40` bytes, but I'm not really sure what they do * TitleID (u64) at `0xC`, for example `00a0 df10 501f 0001` for Legends: Arceus (the byte order is swapped) * 0x38 bytes of filler starting at offset 0x14 * SaveID (u64) at `0x4C`, for example `0a00 0000 0000 0000` for my Legends: Arceus save * 0x38 bytes of filler starting at offset 0x54 Full example for Legends: Arceus: ``` 000001b0: 494d 454e 4000 0000 4000 0000 00a0 df10 IMEN@...@....... 000001c0: 501f 0001 0100 0000 0000 0000 0000 0000 P............... 000001d0: 0000 0000 0000 0000 0000 0000 0100 0000 ................ 000001e0: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 000001f0: 0000 0000 0000 0000 0000 0000 0a00 0000 ................ 00000200: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00000210: 0000 0000 0100 0000 0000 0000 0000 0000 ................ 00000220: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00000230: 0000 0000 0000 0000 0000 0000 494d 454e ............IMEN ``` Ultimately, the size of the `IMEN` sits at 0x8C or 140 bytes. With this knowledge reading all the TitleID -> SaveID pairs is basically free, and outside of validation and stuff is like 15 lines of relevant code. Some interesting caveats, though: - There are two entries for some TitleIDs for... some reason? Ignoring the second one seems to work though. - Within each save directory, there are directories `0` and `1`... and only `0` ever seems used??? It's where Ryujinx points you to for save, so I just chose to use that. Once everything is parsed, the rest of the implementation is extremely trivial: - When the user requests a Ryujinx link, match the current program_id to the corresponding SaveID in `imkvdb` - If it doesn't exist, just error out (save data is probably nonexistent) - If it does though, give the user the option to use Eden's current save data OR Ryujinx's current save data. Old save data is deleted depending on which one you chose. Signed-off-by: crueter <crueter@eden-emu.dev> Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/2815 Reviewed-by: Lizzie <lizzie@eden-emu.dev> Reviewed-by: MaranBr <maranbr@eden-emu.dev>
372 lines
12 KiB
C++
372 lines
12 KiB
C++
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
#pragma once
|
|
|
|
#include <filesystem>
|
|
#include <vector>
|
|
|
|
#include "common/fs/fs_util.h"
|
|
|
|
namespace Common::FS {
|
|
|
|
enum class EdenPath {
|
|
EdenDir, // Where yuzu stores its data.
|
|
AmiiboDir, // Where Amiibo backups are stored.
|
|
CacheDir, // Where cached filesystem data is stored.
|
|
ConfigDir, // Where config files are stored.
|
|
CrashDumpsDir, // Where crash dumps are stored.
|
|
DumpDir, // Where dumped data is stored.
|
|
KeysDir, // Where key files are stored.
|
|
LoadDir, // Where cheat/mod files are stored.
|
|
LogDir, // Where log files are stored.
|
|
NANDDir, // Where the emulated NAND is stored.
|
|
PlayTimeDir, // Where play time data is stored.
|
|
ScreenshotsDir, // Where yuzu screenshots are stored.
|
|
SDMCDir, // Where the emulated SDMC is stored.
|
|
ShaderDir, // Where shaders are stored.
|
|
TASDir, // Where TAS scripts are stored.
|
|
IconsDir, // Where Icons for Windows shortcuts are stored.
|
|
};
|
|
|
|
// migration/compat dirs
|
|
enum EmuPath {
|
|
CitronDir,
|
|
CitronConfigDir,
|
|
CitronCacheDir,
|
|
|
|
SudachiDir,
|
|
SudachiConfigDir,
|
|
SudachiCacheDir,
|
|
|
|
YuzuDir,
|
|
YuzuConfigDir,
|
|
YuzuCacheDir,
|
|
|
|
SuyuDir,
|
|
SuyuConfigDir,
|
|
SuyuCacheDir,
|
|
|
|
// used exclusively for save data linking
|
|
RyujinxDir,
|
|
};
|
|
|
|
/**
|
|
* Validates a given path.
|
|
*
|
|
* A given path is valid if it meets these conditions:
|
|
* - The path is not empty
|
|
* - The path is not too long
|
|
*
|
|
* @param path Filesystem path
|
|
*
|
|
* @returns True if the path is valid, false otherwise.
|
|
*/
|
|
[[nodiscard]] bool ValidatePath(const std::filesystem::path& path);
|
|
|
|
#ifdef _WIN32
|
|
template <typename Path>
|
|
[[nodiscard]] bool ValidatePath(const Path& path) {
|
|
if constexpr (IsChar<typename Path::value_type>) {
|
|
return ValidatePath(ToU8String(path));
|
|
} else {
|
|
return ValidatePath(std::filesystem::path{path});
|
|
}
|
|
}
|
|
#endif
|
|
|
|
/**
|
|
* Concatenates two filesystem paths together.
|
|
*
|
|
* This is needed since the following occurs when using std::filesystem::path's operator/:
|
|
* first: "/first/path"
|
|
* second: "/second/path" (Note that the second path has a directory separator in the front)
|
|
* first / second yields "/second/path" when the desired result is first/path/second/path
|
|
*
|
|
* @param first First filesystem path
|
|
* @param second Second filesystem path
|
|
*
|
|
* @returns A concatenated filesystem path.
|
|
*/
|
|
[[nodiscard]] std::filesystem::path ConcatPath(const std::filesystem::path& first,
|
|
const std::filesystem::path& second);
|
|
|
|
#ifdef _WIN32
|
|
template <typename Path1, typename Path2>
|
|
[[nodiscard]] std::filesystem::path ConcatPath(const Path1& first, const Path2& second) {
|
|
using ValueType1 = typename Path1::value_type;
|
|
using ValueType2 = typename Path2::value_type;
|
|
if constexpr (IsChar<ValueType1> && IsChar<ValueType2>) {
|
|
return ConcatPath(ToU8String(first), ToU8String(second));
|
|
} else if constexpr (IsChar<ValueType1> && !IsChar<ValueType2>) {
|
|
return ConcatPath(ToU8String(first), second);
|
|
} else if constexpr (!IsChar<ValueType1> && IsChar<ValueType2>) {
|
|
return ConcatPath(first, ToU8String(second));
|
|
} else {
|
|
return ConcatPath(std::filesystem::path{first}, std::filesystem::path{second});
|
|
}
|
|
}
|
|
#endif
|
|
|
|
/**
|
|
* Safe variant of ConcatPath that takes in a base path and an offset path from the given base path.
|
|
*
|
|
* If ConcatPath(base, offset) resolves to a path that is sandboxed within the base path,
|
|
* this will return the concatenated path. Otherwise this will return the base path.
|
|
*
|
|
* @param base Base filesystem path
|
|
* @param offset Offset filesystem path
|
|
*
|
|
* @returns A concatenated filesystem path if it is within the base path,
|
|
* returns the base path otherwise.
|
|
*/
|
|
[[nodiscard]] std::filesystem::path ConcatPathSafe(const std::filesystem::path& base,
|
|
const std::filesystem::path& offset);
|
|
|
|
#ifdef _WIN32
|
|
template <typename Path1, typename Path2>
|
|
[[nodiscard]] std::filesystem::path ConcatPathSafe(const Path1& base, const Path2& offset) {
|
|
using ValueType1 = typename Path1::value_type;
|
|
using ValueType2 = typename Path2::value_type;
|
|
if constexpr (IsChar<ValueType1> && IsChar<ValueType2>) {
|
|
return ConcatPathSafe(ToU8String(base), ToU8String(offset));
|
|
} else if constexpr (IsChar<ValueType1> && !IsChar<ValueType2>) {
|
|
return ConcatPathSafe(ToU8String(base), offset);
|
|
} else if constexpr (!IsChar<ValueType1> && IsChar<ValueType2>) {
|
|
return ConcatPathSafe(base, ToU8String(offset));
|
|
} else {
|
|
return ConcatPathSafe(std::filesystem::path{base}, std::filesystem::path{offset});
|
|
}
|
|
}
|
|
#endif
|
|
|
|
/**
|
|
* Checks whether a given path is sandboxed within a given base path.
|
|
*
|
|
* @param base Base filesystem path
|
|
* @param path Filesystem path
|
|
*
|
|
* @returns True if the given path is sandboxed within the given base path, false otherwise.
|
|
*/
|
|
[[nodiscard]] bool IsPathSandboxed(const std::filesystem::path& base,
|
|
const std::filesystem::path& path);
|
|
|
|
#ifdef _WIN32
|
|
template <typename Path1, typename Path2>
|
|
[[nodiscard]] bool IsPathSandboxed(const Path1& base, const Path2& path) {
|
|
using ValueType1 = typename Path1::value_type;
|
|
using ValueType2 = typename Path2::value_type;
|
|
if constexpr (IsChar<ValueType1> && IsChar<ValueType2>) {
|
|
return IsPathSandboxed(ToU8String(base), ToU8String(path));
|
|
} else if constexpr (IsChar<ValueType1> && !IsChar<ValueType2>) {
|
|
return IsPathSandboxed(ToU8String(base), path);
|
|
} else if constexpr (!IsChar<ValueType1> && IsChar<ValueType2>) {
|
|
return IsPathSandboxed(base, ToU8String(path));
|
|
} else {
|
|
return IsPathSandboxed(std::filesystem::path{base}, std::filesystem::path{path});
|
|
}
|
|
}
|
|
#endif
|
|
|
|
/**
|
|
* Checks if a character is a directory separator (either a forward slash or backslash).
|
|
*
|
|
* @param character Character
|
|
*
|
|
* @returns True if the character is a directory separator, false otherwise.
|
|
*/
|
|
[[nodiscard]] bool IsDirSeparator(char character);
|
|
|
|
/**
|
|
* Checks if a character is a directory separator (either a forward slash or backslash).
|
|
*
|
|
* @param character Character
|
|
*
|
|
* @returns True if the character is a directory separator, false otherwise.
|
|
*/
|
|
[[nodiscard]] bool IsDirSeparator(char8_t character);
|
|
|
|
/**
|
|
* Removes any trailing directory separators if they exist in the given path.
|
|
*
|
|
* @param path Filesystem path
|
|
*
|
|
* @returns The filesystem path without any trailing directory separators.
|
|
*/
|
|
[[nodiscard]] std::filesystem::path RemoveTrailingSeparators(const std::filesystem::path& path);
|
|
|
|
#ifdef _WIN32
|
|
template <typename Path>
|
|
[[nodiscard]] std::filesystem::path RemoveTrailingSeparators(const Path& path) {
|
|
if constexpr (IsChar<typename Path::value_type>) {
|
|
return RemoveTrailingSeparators(ToU8String(path));
|
|
} else {
|
|
return RemoveTrailingSeparators(std::filesystem::path{path});
|
|
}
|
|
}
|
|
#endif
|
|
|
|
/**
|
|
* Sets the directory used for application storage. Used on Android where we do not know internal
|
|
* storage until informed by the frontend.
|
|
*
|
|
* @param app_directory Directory to use for application storage.
|
|
*/
|
|
void SetAppDirectory(const std::string& app_directory);
|
|
|
|
/**
|
|
* Gets the filesystem path associated with the EdenPath enum.
|
|
*
|
|
* @param eden_path EdenPath enum
|
|
*
|
|
* @returns The filesystem path associated with the EdenPath enum.
|
|
*/
|
|
[[nodiscard]] const std::filesystem::path& GetEdenPath(EdenPath eden_path);
|
|
|
|
/**
|
|
* Gets the filesystem path associated with the LegacyPath enum.
|
|
*
|
|
* @param legacy_path LegacyPath enum
|
|
*
|
|
* @returns The filesystem path associated with the LegacyPath enum.
|
|
*/
|
|
[[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.
|
|
*
|
|
* @param eden_path EdenPath enum
|
|
*
|
|
* @returns The filesystem path associated with the EdenPath enum as a UTF-8 encoded std::string.
|
|
*/
|
|
[[nodiscard]] std::string GetEdenPathString(EdenPath eden_path);
|
|
|
|
/**
|
|
* Gets the filesystem path associated with the LegacyPath enum as a UTF-8 encoded std::string.
|
|
*
|
|
* @param legacy_path LegacyPath enum
|
|
*
|
|
* @returns The filesystem path associated with the LegacyPath enum as a UTF-8 encoded std::string.
|
|
*/
|
|
[[nodiscard]] std::string GetLegacyPathString(EmuPath legacy_path);
|
|
|
|
/**
|
|
* Sets a new filesystem path associated with the EdenPath enum.
|
|
* If the filesystem object at new_path is not a directory, this function will not do anything.
|
|
*
|
|
* @param eden_path EdenPath enum
|
|
* @param new_path New filesystem path
|
|
*/
|
|
void SetEdenPath(EdenPath eden_path, const std::filesystem::path& new_path);
|
|
|
|
/**
|
|
* Creates all necessary Eden paths in the filesystem.
|
|
*/
|
|
void CreateEdenPaths();
|
|
|
|
#ifdef _WIN32
|
|
template <typename Path>
|
|
void SetEdenPath(EdenPath eden_path, const Path& new_path) {
|
|
if constexpr (IsChar<typename Path::value_type>) {
|
|
SetEdenPath(eden_path, ToU8String(new_path));
|
|
} else {
|
|
SetEdenPath(eden_path, std::filesystem::path{new_path});
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#ifdef _WIN32
|
|
|
|
/**
|
|
* Gets the path of the directory containing the executable of the current process.
|
|
*
|
|
* @returns The path of the directory containing the executable of the current process.
|
|
*/
|
|
[[nodiscard]] std::filesystem::path GetExeDirectory();
|
|
|
|
/**
|
|
* Gets the path of the current user's %APPDATA% directory (%USERPROFILE%/AppData/Roaming).
|
|
*
|
|
* @returns The path of the current user's %APPDATA% directory.
|
|
*/
|
|
[[nodiscard]] std::filesystem::path GetAppDataRoamingDirectory();
|
|
|
|
#else
|
|
|
|
/**
|
|
* Gets the path of the directory specified by the #HOME environment variable.
|
|
* If $HOME is not defined, it will attempt to query the user database in passwd instead.
|
|
*
|
|
* @returns The path of the current user's home directory.
|
|
*/
|
|
[[nodiscard]] std::filesystem::path GetHomeDirectory();
|
|
|
|
/**
|
|
* Gets the relevant paths for yuzu to store its data based on the given XDG environment variable.
|
|
* See https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
|
|
* Defaults to $HOME/.local/share for main application data,
|
|
* $HOME/.cache for cached data, and $HOME/.config for configuration files.
|
|
*
|
|
* @param env_name XDG environment variable name
|
|
*
|
|
* @returns The path where yuzu should store its data.
|
|
*/
|
|
[[nodiscard]] std::filesystem::path GetDataDirectory(const std::string& env_name);
|
|
|
|
#endif
|
|
|
|
#ifdef __APPLE__
|
|
|
|
[[nodiscard]] std::filesystem::path GetBundleDirectory();
|
|
|
|
#endif
|
|
|
|
// vvvvvvvvvv Deprecated vvvvvvvvvv //
|
|
|
|
// Removes the final '/' or '\' if one exists
|
|
[[nodiscard]] std::string_view RemoveTrailingSlash(std::string_view path);
|
|
|
|
enum class DirectorySeparator {
|
|
ForwardSlash,
|
|
BackwardSlash,
|
|
PlatformDefault,
|
|
};
|
|
|
|
// Splits the path on '/' or '\' and put the components into a vector
|
|
// i.e. "C:\Users\Yuzu\Documents\save.bin" becomes {"C:", "Users", "Yuzu", "Documents", "save.bin" }
|
|
[[nodiscard]] std::vector<std::string_view> SplitPathComponents(std::string_view filename);
|
|
|
|
// Splits the path on '/' or '\' and put the components into a vector
|
|
// i.e. "C:\Users\Yuzu\Documents\save.bin" becomes {"C:", "Users", "Yuzu", "Documents", "save.bin" }
|
|
[[nodiscard]] std::vector<std::string> SplitPathComponentsCopy(std::string_view filename);
|
|
|
|
// Removes trailing slash, makes all '\\' into '/', and removes duplicate '/'. Makes '/' into '\\'
|
|
// depending if directory_separator is BackwardSlash or PlatformDefault and running on windows
|
|
[[nodiscard]] std::string SanitizePath(
|
|
std::string_view path,
|
|
DirectorySeparator directory_separator = DirectorySeparator::ForwardSlash);
|
|
|
|
// Gets all of the text up to the last '/' or '\' in the path.
|
|
[[nodiscard]] std::string GetParentPath(std::string_view path);
|
|
|
|
// Gets all of the text after the first '/' or '\' in the path.
|
|
[[nodiscard]] std::string_view GetPathWithoutTop(std::string_view path);
|
|
|
|
// Gets the filename of the path
|
|
[[nodiscard]] inline std::string_view GetFilename(const std::string_view path) noexcept {
|
|
if (auto const name_index = path.find_last_of("\\/"); name_index != std::string_view::npos)
|
|
return path.substr(name_index + 1);
|
|
return {};
|
|
}
|
|
|
|
// Gets the extension of the filename
|
|
[[nodiscard]] inline std::string_view GetExtensionFromFilename(const std::string_view name) noexcept {
|
|
if (auto const index = name.rfind('.'); index != std::string_view::npos)
|
|
return name.substr(index + 1);
|
|
return {};
|
|
}
|
|
|
|
} // namespace Common::FS
|