diff --git a/emulators/eden.yml b/emulators/eden.yml index 2f8b58c8e..80ba11f1f 100644 --- a/emulators/eden.yml +++ b/emulators/eden.yml @@ -63,6 +63,7 @@ files: standalone_directories: - "$XDG_DATA_HOME/eden" - "$HOME/.local/share/eden" + - "PORTABLE_DIR" data_root: windows: path: "%APPDATA%/eden" diff --git a/emulators/ryujinx.yml b/emulators/ryujinx.yml index 74a9b5b43..aa3d4df08 100644 --- a/emulators/ryujinx.yml +++ b/emulators/ryujinx.yml @@ -19,6 +19,7 @@ mode: standalone standalone_directories: - "$XDG_CONFIG_HOME/Ryujinx" - "$HOME/.config/Ryujinx" + - "DefaultPortableDir" data_root: windows: path: "%APPDATA%/Ryujinx" diff --git a/emulators/yuzu.yml b/emulators/yuzu.yml index 77ec2ef35..7aafef9c5 100644 --- a/emulators/yuzu.yml +++ b/emulators/yuzu.yml @@ -59,6 +59,7 @@ files: standalone_directories: - "$XDG_DATA_HOME/yuzu" - "$HOME/.local/share/yuzu" + - "PORTABLE_DIR" data_root: windows: path: "%APPDATA%/yuzu" diff --git a/platforms/retrobat.yml b/platforms/retrobat.yml index 7da5f851f..1a89d0f1c 100644 --- a/platforms/retrobat.yml +++ b/platforms/retrobat.yml @@ -4971,17 +4971,42 @@ launchers: kind: passes_path names: [] search_dir: "/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: "/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: "/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: "/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: "/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 diff --git a/scripts/scraper/retrobat_launchers.py b/scripts/scraper/retrobat_launchers.py index 45ce221b2..6a8f178f6 100644 --- a/scripts/scraper/retrobat_launchers.py +++ b/scripts/scraper/retrobat_launchers.py @@ -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 /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 /user (/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'/{user.group(1)}', - passes_to=f"PORTABLE_DIR, the user folder beside {binary}", + source_ref((src.path, spans)), + search_dir=f'/{folder.group(2)}', + passes_to=f"{handle}, the portable folder beside {binary}", fails_when_missing=False, + depends_on_option=depends, )) return records diff --git a/tests/test_launcher_paths.py b/tests/test_launcher_paths.py index 261de19b8..e2b8a6866 100644 --- a/tests/test_launcher_paths.py +++ b/tests/test_launcher_paths.py @@ -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", '/user', "c:3, 7-8"), - ("shadps4", '/user', "s:3, 8-10")], + ("eden", '/user', "e:3, 7-8"), + ("ryujinx", '/portable', "r:3-5"), + ("shadps4", '/user', "s:3, 8-10"), + ("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):