Browse Source
WIP: [qt] Ryujinx save data link
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 <crueter@eden-emu.dev>pull/2815/head
23 changed files with 493 additions and 32 deletions
-
1src/common/CMakeLists.txt
-
1src/common/fs/fs_paths.h
-
28src/common/fs/path_util.cpp
-
18src/common/fs/path_util.h
-
97src/common/ryujinx_compat.cpp
-
40src/common/ryujinx_compat.h
-
12src/frontend_common/data_manager.cpp
-
4src/frontend_common/data_manager.h
-
36src/qt_common/qt_common.cpp
-
2src/qt_common/qt_common.h
-
23src/qt_common/qt_string_lookup.h
-
4src/qt_common/util/content.cpp
-
6src/qt_common/util/content.h
-
1src/yuzu/CMakeLists.txt
-
2src/yuzu/data_dialog.cpp
-
5src/yuzu/game_list.cpp
-
1src/yuzu/game_list.h
-
55src/yuzu/main.cpp
-
3src/yuzu/main.h
-
6src/yuzu/migration_worker.h
-
65src/yuzu/ryujinx_dialog.cpp
-
34src/yuzu/ryujinx_dialog.h
-
81src/yuzu/ryujinx_dialog.ui
@ -0,0 +1,97 @@ |
|||||
|
#include "ryujinx_compat.h"
|
||||
|
#include "common/fs/path_util.h"
|
||||
|
#include <cstddef>
|
||||
|
#include <cstring>
|
||||
|
#include <fmt/ranges.h>
|
||||
|
#include <fstream>
|
||||
|
#include <iostream>
|
||||
|
|
||||
|
namespace Common::FS { |
||||
|
|
||||
|
namespace fs = std::filesystem; |
||||
|
|
||||
|
fs::path GetKvdbPath() |
||||
|
{ |
||||
|
return GetLegacyPath(EmuPath::RyujinxDir) / "bis" / "system" / "save" / "8000000000000000" / "0" |
||||
|
/ "imkvdb.arc"; |
||||
|
} |
||||
|
|
||||
|
fs::path GetRyuSavePath(const u64 &save_id) |
||||
|
{ |
||||
|
std::string hex = fmt::format("{:016x}", save_id); |
||||
|
|
||||
|
// TODO: what's the difference between 0 and 1?
|
||||
|
return GetLegacyPath(EmuPath::RyujinxDir) / "bis" / "user" / "save" / hex / "0"; |
||||
|
} |
||||
|
|
||||
|
IMENReadResult ReadKvdb(const fs::path &path, std::vector<IMEN> &imens) |
||||
|
{ |
||||
|
std::ifstream kvdb{path, std::ios::binary | std::ios::ate}; |
||||
|
|
||||
|
// 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<char *>(&title_id), 8); |
||||
|
kvdb.ignore(0x38); |
||||
|
kvdb.read(reinterpret_cast<char *>(&save_id), 8); |
||||
|
kvdb.ignore(0x38); |
||||
|
|
||||
|
imens[i / 2] = IMEN{ title_id, save_id}; |
||||
|
} |
||||
|
|
||||
|
return IMENReadResult::Success; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,40 @@ |
|||||
|
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project |
||||
|
// SPDX-License-Identifier: GPL-3.0-or-later |
||||
|
|
||||
|
#pragma once |
||||
|
|
||||
|
#include "common/common_types.h" |
||||
|
#include <filesystem> |
||||
|
#include <vector> |
||||
|
|
||||
|
namespace fs = std::filesystem; |
||||
|
|
||||
|
namespace Common::FS { |
||||
|
|
||||
|
constexpr const char IMEN_MAGIC[4] = {0x49, 0x4d, 0x45, 0x4e}; |
||||
|
constexpr const char IMKV_MAGIC[4] = {0x49, 0x4d, 0x4b, 0x56}; |
||||
|
constexpr const u8 IMEN_SIZE = 0x8c; |
||||
|
|
||||
|
fs::path GetKvdbPath(); |
||||
|
fs::path GetRyuSavePath(const u64 &program_id); |
||||
|
|
||||
|
enum class IMENReadResult { |
||||
|
Nonexistent, // ryujinx not found |
||||
|
NoHeader, // file isn't big enough for header |
||||
|
InvalidMagic, // no IMKV or IMEN header |
||||
|
Misaligned, // file isn't aligned to expected IMEN boundaries |
||||
|
NoImens, // no-op, there are no IMENs |
||||
|
Success, // :) |
||||
|
}; |
||||
|
|
||||
|
struct IMEN |
||||
|
{ |
||||
|
u64 title_id; |
||||
|
u64 save_id; |
||||
|
}; |
||||
|
|
||||
|
static_assert(sizeof(IMEN) == 0x10, "IMEN has incorrect size."); |
||||
|
|
||||
|
IMENReadResult ReadKvdb(const fs::path &path, std::vector<IMEN> &imens); |
||||
|
|
||||
|
} // namespace Common::FS |
||||
@ -0,0 +1,65 @@ |
|||||
|
#include "qt_common/abstract/qt_frontend_util.h"
|
||||
|
#include "ryujinx_dialog.h"
|
||||
|
#include "ui_ryujinx_dialog.h"
|
||||
|
#include <filesystem>
|
||||
|
#include <system_error>
|
||||
|
#include <fmt/format.h>
|
||||
|
|
||||
|
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.")); |
||||
|
} |
||||
@ -0,0 +1,34 @@ |
|||||
|
#ifndef RYUJINX_DIALOG_H |
||||
|
#define RYUJINX_DIALOG_H |
||||
|
|
||||
|
#include <QDialog> |
||||
|
#include <filesystem> |
||||
|
|
||||
|
namespace Ui { |
||||
|
class RyujinxDialog; |
||||
|
} |
||||
|
|
||||
|
class RyujinxDialog : public QDialog |
||||
|
{ |
||||
|
Q_OBJECT |
||||
|
|
||||
|
public: |
||||
|
explicit RyujinxDialog(std::filesystem::path eden_path, std::filesystem::path ryu_path, QWidget *parent = nullptr); |
||||
|
~RyujinxDialog(); |
||||
|
|
||||
|
private slots: |
||||
|
void fromEden(); |
||||
|
void fromRyujinx(); |
||||
|
|
||||
|
private: |
||||
|
Ui::RyujinxDialog *ui; |
||||
|
std::filesystem::path m_eden; |
||||
|
std::filesystem::path m_ryu; |
||||
|
|
||||
|
/// @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 |
||||
@ -0,0 +1,81 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<ui version="4.0"> |
||||
|
<class>RyujinxDialog</class> |
||||
|
<widget class="QDialog" name="RyujinxDialog"> |
||||
|
<property name="geometry"> |
||||
|
<rect> |
||||
|
<x>0</x> |
||||
|
<y>0</y> |
||||
|
<width>404</width> |
||||
|
<height>170</height> |
||||
|
</rect> |
||||
|
</property> |
||||
|
<property name="windowTitle"> |
||||
|
<string>Ryujinx Link</string> |
||||
|
</property> |
||||
|
<layout class="QVBoxLayout" name="verticalLayout"> |
||||
|
<item> |
||||
|
<widget class="QLabel" name="label"> |
||||
|
<property name="sizePolicy"> |
||||
|
<sizepolicy hsizetype="Preferred" vsizetype="Expanding"> |
||||
|
<horstretch>0</horstretch> |
||||
|
<verstretch>0</verstretch> |
||||
|
</sizepolicy> |
||||
|
</property> |
||||
|
<property name="text"> |
||||
|
<string>Linking save data to Ryujinx lets both Ryujinx and Eden reference the same save files for your games. |
||||
|
|
||||
|
By selecting "From Eden", previous save data stored in Ryujinx will be deleted, and vice versa for "From Ryujinx".</string> |
||||
|
</property> |
||||
|
<property name="wordWrap"> |
||||
|
<bool>true</bool> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item> |
||||
|
<layout class="QHBoxLayout" name="horizontalLayout"> |
||||
|
<item> |
||||
|
<widget class="QPushButton" name="eden"> |
||||
|
<property name="text"> |
||||
|
<string>From Eden</string> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item> |
||||
|
<widget class="QPushButton" name="ryujinx"> |
||||
|
<property name="text"> |
||||
|
<string>From Ryujinx</string> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item> |
||||
|
<widget class="QPushButton" name="cancel"> |
||||
|
<property name="text"> |
||||
|
<string>Cancel</string> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
</layout> |
||||
|
</item> |
||||
|
</layout> |
||||
|
</widget> |
||||
|
<resources/> |
||||
|
<connections> |
||||
|
<connection> |
||||
|
<sender>cancel</sender> |
||||
|
<signal>clicked()</signal> |
||||
|
<receiver>RyujinxDialog</receiver> |
||||
|
<slot>reject()</slot> |
||||
|
<hints> |
||||
|
<hint type="sourcelabel"> |
||||
|
<x>331</x> |
||||
|
<y>147</y> |
||||
|
</hint> |
||||
|
<hint type="destinationlabel"> |
||||
|
<x>201</x> |
||||
|
<y>84</y> |
||||
|
</hint> |
||||
|
</hints> |
||||
|
</connection> |
||||
|
</connections> |
||||
|
</ui> |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue