[qt] Ryujinx save data link #2815

Merged
MaranBr merged 10 commits from ryujinx-compat into master 2025-10-28 03:46:48 +01:00
Owner

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

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>
WIP: [qt] Ryujinx save data link
Some checks reported errors
eden-license / license-header (pull_request) Failing after 29s
GitHub Actions [CI] Build cancelled
83744266aa
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>
fix licens eheaders
Some checks failed
eden-license / license-header (pull_request) Successful in 29s
GitHub Actions [CI] Build failed
37e4a4d560
Signed-off-by: crueter <crueter@eden-emu.dev>
fix windows
All checks were successful
eden-license / license-header (pull_request) Successful in 30s
GitHub Actions [CI] Build succeeded
GitHub Releases [CD] Build succeeded – Release published
94af5895d1
Signed-off-by: crueter <crueter@eden-emu.dev>
proper error handling, do not ignore every other
Some checks failed
eden-license / license-header (pull_request) Successful in 29s
GitHub Actions [CI] Build failed
b18649818a
Signed-off-by: crueter <crueter@eden-emu.dev>
crueter force-pushed ryujinx-compat from b18649818a
Some checks failed
eden-license / license-header (pull_request) Successful in 29s
GitHub Actions [CI] Build failed
to 098f6feae5
Some checks failed
eden-license / license-header (pull_request) Successful in 29s
GitHub Actions [CI] Build succeeded
GitHub Releases [CD] Build succeeded – Release published
build.yml / fix mklink (pull_request) Failing after 0s
trigger_release.yml / fix mklink (pull_request) Failing after 0s
2025-10-26 17:48:53 +01:00
Compare
Lizzie approved these changes 2025-10-27 01:21:17 +01:00
Dismissed
MaranBr approved these changes 2025-10-27 01:38:58 +01:00
Dismissed
Signed-off-by: crueter <crueter@eden-emu.dev>
add unlink check
Some checks reported errors
build.yml / add unlink check (push) Failing after 0s
trigger_release.yml / add unlink check (push) Failing after 0s
build.yml / add unlink check (pull_request) Failing after 0s
trigger_release.yml / add unlink check (pull_request) Failing after 0s
eden-license / license-header (pull_request) Successful in 31s
GitHub Actions [CI] Build cancelled
7b0b71d16b
Signed-off-by: crueter <crueter@eden-emu.dev>
crueter dismissed Lizzie's review 2025-10-27 06:41:47 +01:00
Reason:

New commits pushed, approval review dismissed automatically according to repository settings

crueter dismissed MaranBr's review 2025-10-27 06:41:47 +01:00
Reason:

New commits pushed, approval review dismissed automatically according to repository settings

make windows work
Some checks reported errors
build.yml / make windows work (push) Failing after 0s
trigger_release.yml / make windows work (push) Failing after 0s
build.yml / make windows work (pull_request) Failing after 0s
trigger_release.yml / make windows work (pull_request) Failing after 0s
eden-license / license-header (pull_request) Successful in 31s
GitHub Actions [CI] Build cancelled
8d974baf0d
Signed-off-by: crueter <crueter@eden-emu.dev>
crueter force-pushed ryujinx-compat from 8d974baf0d
Some checks reported errors
build.yml / make windows work (push) Failing after 0s
trigger_release.yml / make windows work (push) Failing after 0s
build.yml / make windows work (pull_request) Failing after 0s
trigger_release.yml / make windows work (pull_request) Failing after 0s
eden-license / license-header (pull_request) Successful in 31s
GitHub Actions [CI] Build cancelled
to 5867a1a1f9
All checks were successful
eden-license / license-header (pull_request) Successful in 29s
GitHub Actions [CI] Build succeeded
GitHub Releases [CD] Build succeeded – Release published
2025-10-27 07:01:42 +01:00
Compare
crueter changed title from WIP: [qt] Ryujinx save data link to [qt] Ryujinx save data link 2025-10-27 07:01:49 +01:00
crueter added this to the 0.0.4 (real) milestone 2025-10-27 08:00:36 +01:00
okay fuck you microsoft
Some checks failed
eden-license / license-header (pull_request) Successful in 32s
GitHub Actions [CI] Build failed
3d4b3f80c4
Signed-off-by: crueter <crueter@eden-emu.dev>
fix build
All checks were successful
eden-license / license-header (pull_request) Successful in 35s
GitHub Actions [CI] Build succeeded
GitHub Releases [CD] Build succeeded – Release published
74072c8804
Signed-off-by: crueter <crueter@eden-emu.dev>
Lizzie approved these changes 2025-10-27 21:00:50 +01:00
MaranBr approved these changes 2025-10-28 03:45:26 +01:00
MaranBr merged commit 39f226a853 into master 2025-10-28 03:46:48 +01:00
MaranBr deleted branch ryujinx-compat 2025-10-28 03:46:48 +01:00
MaranBr referenced this pull request from a commit 2025-10-28 03:46:50 +01:00
Sign in to join this conversation.
No description provided.