feat: bind retrobat switch portable folders

RetroBat's generators create user/ beside yuzu.exe and eden.exe and
portable/ beside Ryujinx.exe, which the emulators read in place of
%APPDATA% (PORTABLE_DIR, Ryujinx AppDataManager.cs:12, 60). Their keys
now go below emulators/<key>/.
This commit is contained in:
Abdessamad Derraz committed 2026-11-09 19:54:00 +01:00
1 parent 7e22c38113
commit 111a93440f
6 files changed
+100 -26

No files matched your search

+1
View File
@@ -63,6 +63,7 @@ files:
standalone_directories:
- "$XDG_DATA_HOME/eden"
- "$HOME/.local/share/eden"
- "PORTABLE_DIR"
data_root:
windows:
path: "%APPDATA%/eden"
+1
View File
@@ -19,6 +19,7 @@ mode: standalone
standalone_directories:
- "$XDG_CONFIG_HOME/Ryujinx"
- "$HOME/.config/Ryujinx"
- "DefaultPortableDir"
data_root:
windows:
path: "%APPDATA%/Ryujinx"
+1
View File
@@ -59,6 +59,7 @@ files:
standalone_directories:
- "$XDG_DATA_HOME/yuzu"
- "$HOME/.local/share/yuzu"
- "PORTABLE_DIR"
data_root:
windows:
path: "%APPDATA%/yuzu"
+27 -2
View File
@@ -4971,17 +4971,42 @@ launchers:
kind: passes_path
names: []
search_dir: "<AppConfig.GetFullPath(\"citron\")>/user"
passes_to: PORTABLE_DIR, the user folder beside citron.exe
passes_to: PORTABLE_DIR, the portable folder beside citron.exe
fails_when_missing: false
source_ref: "emulatorLauncher/Generators/Citron.Generator.cs:25, 29-30"
- emulator: eden
build: standalone
kind: passes_path
names: []
search_dir: "<AppConfig.GetFullPath(\"eden\")>/user"
passes_to: PORTABLE_DIR, the portable folder beside eden.exe
fails_when_missing: false
source_ref: "emulatorLauncher/Generators/Eden.Generator.cs:28, 35-36"
- emulator: ryujinx
build: standalone
kind: passes_path
names: []
search_dir: "<AppConfig.GetFullPath(\"ryujinx\")>/portable"
passes_to: DefaultPortableDir, the portable folder beside Ryujinx.exe
fails_when_missing: false
depends_on_option: unless saves/switch/ryujinx/portable exists, passed with -r
source_ref: "emulatorLauncher/Generators/Ryujinx.Generator.cs:31, 73-75"
- emulator: shadps4
build: standalone
kind: passes_path
names: []
search_dir: "<AppConfig.GetFullPath(\"shadps4\")>/user"
passes_to: PORTABLE_DIR, the user folder beside shadPS4.exe
passes_to: PORTABLE_DIR, the portable folder beside shadPS4.exe
fails_when_missing: false
source_ref: "emulatorLauncher/Generators/ShadPS4.Generator.cs:29, 158-160"
- emulator: yuzu
build: standalone
kind: passes_path
names: []
search_dir: "<AppConfig.GetFullPath(\"yuzu\")>/user"
passes_to: PORTABLE_DIR, the portable folder beside yuzu.exe
fails_when_missing: false
source_ref: "emulatorLauncher/Generators/Yuzu.Generator.cs:24, 33-34"
- emulator: bizhawk
build: standalone
kind: names_searched
+41 -19
View File
@@ -32,6 +32,7 @@ SOURCE_FILES = (
f"{GENERATORS}/DosBoxPure.Generator.cs",
f"{GENERATORS}/DosBoxStaging.Generator.cs",
f"{GENERATORS}/Duckstation.Generator.cs",
f"{GENERATORS}/Eden.Generator.cs",
f"{GENERATORS}/Eka2l1.Generator.cs",
f"{GENERATORS}/Fbneo.Generator.cs",
f"{GENERATORS}/Flycast.Generator.cs",
@@ -57,6 +58,7 @@ SOURCE_FILES = (
f"{GENERATORS}/Project64.Generator.cs",
f"{GENERATORS}/Raine.Generator.cs",
f"{GENERATORS}/Rpcs3.Generator.cs",
f"{GENERATORS}/Ryujinx.Generator.cs",
f"{GENERATORS}/SSF.Generator.cs",
f"{GENERATORS}/ScummVm.Generator.cs",
f"{GENERATORS}/ShadPS4.Generator.cs",
@@ -68,6 +70,7 @@ SOURCE_FILES = (
f"{GENERATORS}/Xroar.Generator.cs",
f"{GENERATORS}/Yabasanshiro.Generator.cs",
f"{GENERATORS}/Ymir.Generator.cs",
f"{GENERATORS}/Yuzu.Generator.cs",
f"{GENERATORS}/mGBA.Generator.cs",
FEATURES,
)
@@ -2982,44 +2985,63 @@ def xroar(sources: dict[str, Source]) -> list[Record]:
return _covered(src, records)
# Generators that create a portable user folder beside the emulator's binary,
# and the emulator each one starts: the folder AppConfig.GetFullPath gives.
# Generators that start an emulator with a portable data folder beside its
# binary: (generator, emulator key, binary, the code's name for the folder).
PORTABLE_USERS = (
("Citron.Generator.cs", "citron", "citron.exe"),
("ShadPS4.Generator.cs", "shadps4", "shadPS4.exe"),
("Citron.Generator.cs", "citron", "citron.exe", "PORTABLE_DIR"),
("Eden.Generator.cs", "eden", "eden.exe", "PORTABLE_DIR"),
("Ryujinx.Generator.cs", "ryujinx", "Ryujinx.exe", "DefaultPortableDir"),
("ShadPS4.Generator.cs", "shadps4", "shadPS4.exe", "PORTABLE_DIR"),
("Yuzu.Generator.cs", "yuzu", "yuzu.exe", "PORTABLE_DIR"),
)
_EMULATOR_FOLDER = (
r"GetEmulatorPathAndExe\(emulator, out string path, out string exe, \"[^\"]+\"\)"
r"|string path = AppConfig\.GetFullPath\(emulator\);"
r"|string path = AppConfig\.GetFullPath\((?:emulator(?:\.Replace\(\"-\", \" \"\))?"
r"|\"\w+\")\);"
)
_PORTABLE_FOLDER = r'string (userFolder|setupPath) = Path\.Combine\(path, "(\w+)"\);'
# Ryujinx's generator hands --root-data-dir saves/switch/ryujinx/portable
# instead when that folder exists.
_ALTERNATE_ROOT = (
r'string portablePath = Path\.Combine\(AppConfig\.GetFullPath\("saves"\), "switch", '
r'"ryujinx", "portable"\);'
)
def portable_users(sources: dict[str, Source]) -> list[Record]:
"""The portable user folder a generator creates beside the binary.
"""The portable data folder a generator gives an emulator beside its binary.
The emulator folder comes from AppConfig.GetFullPath(emulator), and the
generator creates <folder>/user before launching: the emulator then
keeps its data there (keys/, sys_modules/) in place of %APPDATA%.
The emulator folder comes from AppConfig.GetFullPath, and the generator
creates or configures <folder>/user (<folder>/portable for Ryujinx)
before launching: the emulator then keeps its data there (keys/,
sys_modules/, system/) in place of %APPDATA%.
"""
records = []
for name, emulator, binary in PORTABLE_USERS:
for name, emulator, binary, handle in PORTABLE_USERS:
src = sources[f"{GENERATORS}/{name}"]
found = src.find(_EMULATOR_FOLDER, f"{emulator} folder")
user = src.find(r'string userFolder = Path\.Combine\(path, "(\w+)"\);', "user folder")
create = src.find(
r"Directory\.CreateDirectory\(userFolder\)", "user folder creation", user.end()
)
folder = src.find(_PORTABLE_FOLDER, "portable folder")
spans = [src.lines(found.start(), found.end()), src.lines(folder.start(), folder.end())]
if folder.group(1) == "userFolder":
create = src.find(
r"Directory\.CreateDirectory\(userFolder\)", "user folder creation", folder.end()
)
spans[-1] = src.lines(folder.start(), create.end())
depends = None
alternate = re.search(_ALTERNATE_ROOT, src.text)
if alternate:
spans.append(src.lines(alternate.start(), alternate.end()))
depends = "unless saves/switch/ryujinx/portable exists, passed with -r"
records.append(_record(
emulator,
"standalone",
"passes_path",
[],
source_ref((src.path, [
src.lines(found.start(), found.end()), src.lines(user.start(), create.end()),
])),
search_dir=f'<AppConfig.GetFullPath("{emulator}")>/{user.group(1)}',
passes_to=f"PORTABLE_DIR, the user folder beside {binary}",
source_ref((src.path, spans)),
search_dir=f'<AppConfig.GetFullPath("{emulator}")>/{folder.group(2)}',
passes_to=f"{handle}, the portable folder beside {binary}",
fails_when_missing=False,
depends_on_option=depends,
))
return records
+29 -5
View File
@@ -176,12 +176,28 @@ class APortableUserFolder(unittest.TestCase):
}
"""
RYUJINX = """
public override System.Diagnostics.ProcessStartInfo Generate(string system, string emulator, string core, string rom, string playersControllers, ScreenResolution resolution)
{
string path = AppConfig.GetFullPath("ryujinx");
string portablePath = Path.Combine(AppConfig.GetFullPath("saves"), "switch", "ryujinx", "portable");
string setupPath = Path.Combine(path, "portable");
}
"""
def sources(self) -> dict[str, rl.Source]:
def source(ref: str, text: str) -> rl.Source:
return rl.Source(ref, textwrap.dedent(text).lstrip("\n"))
def yuzu_like(exe: str) -> str:
return self.CITRON.replace("citron.exe", exe)
return {
f"{rl.GENERATORS}/Citron.Generator.cs": rl.Source(
"c", textwrap.dedent(self.CITRON).lstrip("\n")),
f"{rl.GENERATORS}/ShadPS4.Generator.cs": rl.Source(
"s", textwrap.dedent(self.SHADPS4).lstrip("\n")),
f"{rl.GENERATORS}/Citron.Generator.cs": source("c", self.CITRON),
f"{rl.GENERATORS}/Eden.Generator.cs": source("e", yuzu_like("eden.exe")),
f"{rl.GENERATORS}/Ryujinx.Generator.cs": source("r", self.RYUJINX),
f"{rl.GENERATORS}/ShadPS4.Generator.cs": source("s", self.SHADPS4),
f"{rl.GENERATORS}/Yuzu.Generator.cs": source("y", yuzu_like("yuzu.exe")),
}
def test_each_generator_gives_its_user_folder(self):
@@ -189,7 +205,15 @@ class APortableUserFolder(unittest.TestCase):
self.assertEqual(
[(r["emulator"], r["search_dir"], r["source_ref"]) for r in found],
[("citron", '<AppConfig.GetFullPath("citron")>/user', "c:3, 7-8"),
("shadps4", '<AppConfig.GetFullPath("shadps4")>/user', "s:3, 8-10")],
("eden", '<AppConfig.GetFullPath("eden")>/user', "e:3, 7-8"),
("ryujinx", '<AppConfig.GetFullPath("ryujinx")>/portable', "r:3-5"),
("shadps4", '<AppConfig.GetFullPath("shadps4")>/user', "s:3, 8-10"),
("yuzu", '<AppConfig.GetFullPath("yuzu")>/user', "y:3, 7-8")],
)
ryujinx = found[2]
self.assertEqual(ryujinx["passes_to"], "DefaultPortableDir, the portable folder beside Ryujinx.exe")
self.assertEqual(
ryujinx["depends_on_option"], "unless saves/switch/ryujinx/portable exists, passed with -r"
)
def test_the_user_folder_binds_below_the_emulator_folder(self):