diff --git a/platforms/retrobat.yml b/platforms/retrobat.yml index 7cbb89ea0..b0589c975 100644 --- a/platforms/retrobat.yml +++ b/platforms/retrobat.yml @@ -4953,6 +4953,16 @@ launchers: fails_when_missing: ApplicationException 'No valid PS2 BIOS file found in ''.' depends_on_option: lrps2_bios unset source_ref: "emulatorLauncher/Generators/LibRetro.CoreOptions.cs:3662-3683, 3708-3713" +- emulator: pico8 + build: standalone + kind: fixed_name + names: + - pico8.exe + case_insensitive: true + search_dir: "" + passes_to: exec + fails_when_missing: returns no process when the binary is absent + source_ref: "emulatorLauncher/Generators/Pico8.Generator.cs:14-18, 56-57" - emulator: bizhawk build: standalone kind: names_searched diff --git a/platforms/rocknix.yml b/platforms/rocknix.yml index c399951a7..88a0f42fa 100644 --- a/platforms/rocknix.yml +++ b/platforms/rocknix.yml @@ -736,6 +736,19 @@ launchers: source_ref: "projects/ROCKNIX/packages/emulators/standalone/armsx2-sa/config/S922X/ARMSX2/inis/PCSX2.ini:21,\ \ 475, projects/ROCKNIX/packages/emulators/standalone/armsx2-sa/config/inputplumber/ARMSX2/inis/PCSX2.ini:21,\ \ 473, projects/ROCKNIX/packages/emulators/standalone/armsx2-sa/package.mk:117-118" +- emulator: pico-8 + build: standalone + kind: fixed_name + names: + - pico8_64 + - pico8_dyn + search_dir: /storage/roms/pico-8 + passes_to: exec + fails_when_missing: false + depends_on_option: "HW_ARCH aarch64 runs pico8_64, any other pico8_dyn, from /storage/roms/pico-8/${HW_ARCH}\ + \ when that folder exists" + source_ref: "projects/ROCKNIX/packages/emulators/standalone/pico-8/scripts/start_pico8.sh:8-15,\ + \ 35-40, 48" - emulator: flycast build: standalone kind: passes_path diff --git a/scripts/scraper/retrobat_launchers.py b/scripts/scraper/retrobat_launchers.py index 6ef1898a5..12f6bba2a 100644 --- a/scripts/scraper/retrobat_launchers.py +++ b/scripts/scraper/retrobat_launchers.py @@ -51,6 +51,7 @@ SOURCE_FILES = ( f"{GENERATORS}/OpenMSX.Generator.cs", f"{GENERATORS}/PSXMame.Generator.cs", f"{GENERATORS}/Pcsx2.Generator.cs", + f"{GENERATORS}/Pico8.Generator.cs", f"{GENERATORS}/PortsLauncher.Configuration.cs", f"{GENERATORS}/Project64.Generator.cs", f"{GENERATORS}/Raine.Generator.cs", @@ -2968,6 +2969,44 @@ def xroar(sources: dict[str, Source]) -> list[Record]: return _covered(src, records) +def pico8(sources: dict[str, Source]) -> list[Record]: + """The PICO-8 binary the generator runs, from an emulator folder. + + AppConfig.GetFullPath names a folder of the RetroBat install + (emulatorLauncher.cfg, else emulators/): the binary is never + looked up in the BIOS tree. + """ + src = sources[f"{GENERATORS}/Pico8.Generator.cs"] + folder = src.find(r'string path = AppConfig\.GetFullPath\("(\w+)"\);', "pico8 folder") + exe = src.find( + r'string exe = Path\.Combine\(path, "([^"]+)"\);', "binary path", folder.end() + ) + check = src.find(r"if \(!File\.Exists\(exe\)\)\s*return null;", "binary check", exe.end()) + run = src.find(r"FileName = exe,\s*WorkingDirectory = path,", "binary run", check.end()) + return [ + _record( + "pico8", + "standalone", + "fixed_name", + [exe.group(1)], + source_ref( + ( + src.path, + [ + src.lines(folder.start(), folder.end()), + src.lines(exe.start(), check.end()), + src.lines(run.start(), run.end()), + ], + ) + ), + case_insensitive=True, + search_dir=f"", + passes_to="exec", + fails_when_missing="returns no process when the binary is absent", + ) + ] + + def snes9x(sources: dict[str, Source]) -> list[Record]: src = sources[f"{GENERATORS}/Snes9x.Generator.cs"] entry = src.find(rf'\{{ "([^"]+)", (Path\.Combine\({_BIOS_ROOT}\)) \}}', "bios folder entry") @@ -3236,6 +3275,7 @@ EXTRACTORS: tuple[Callable[[dict[str, Source]], list[Record]], ...] = ( option_paths, pcsx2, pcsx2_libretro, + pico8, psx_cascades, psxmame, raine, diff --git a/scripts/scraper/rocknix_launchers.py b/scripts/scraper/rocknix_launchers.py index f6eae2840..08a4932c2 100644 --- a/scripts/scraper/rocknix_launchers.py +++ b/scripts/scraper/rocknix_launchers.py @@ -575,6 +575,47 @@ def armsx2(tree: RepoTree) -> list[Launcher]: # Start scripts whose ln -s commands put a BIOS path where the emulator reads. +def pico8(tree: RepoTree) -> list[Launcher]: + """The PICO-8 binary start_pico8.sh executes, outside /storage/roms/bios. + + The script picks the binary by HW_ARCH, pico8_64 on aarch64 and + pico8_dyn on any other, and runs it from GAME_DIR, or from + GAME_DIR/${HW_ARCH} when that folder exists. + """ + path = f"{STANDALONE}/pico-8/scripts/start_pico8.sh" + script = Script(path, tree.read(path)) + game_line, game = script.find(r'^\s*GAME_DIR="([^"$]+)"', "GAME_DIR assignment") + case_line, _ = script.find(r"^\s*case \$\{HW_ARCH\} in", "case on HW_ARCH") + arch_line, arch = script.find(r"^\s*(\w+)\)", "HW_ARCH branch", case_line + 1) + first_line, first = script.find(r'STATIC_BIN="([^"$]+)"', "binary of the first branch", arch_line) + other_line, _ = script.find(r"^\s*\*\)", "default HW_ARCH branch", first_line) + rest_line, rest = script.find(r'STATIC_BIN="([^"$]+)"', "binary of the default branch", other_line) + sub_line, _ = script.find( + r'if \[ -d "\$\{GAME_DIR\}/\$\{HW_ARCH\}" \]', "per-architecture launch folder" + ) + end_line, _ = script.find(r"^\s*fi\b", "end of the launch folder test", sub_line) + run_line, _ = script.find(r"^\s*\$\{LAUNCH_DIR\}/\$\{STATIC_BIN\} ", "binary run") + return [ + Launcher( + emulator="pico-8", + build="standalone", + kind="fixed_name", + names=[first.group(1), rest.group(1)], + source_ref=source_ref([(path, { + game_line, case_line, arch_line, first_line, other_line, rest_line, + *range(sub_line, end_line + 1), run_line, + })]), + search_dir=game.group(1).rstrip("/"), + passes_to="exec", + fails_when_missing=False, + depends_on_option=( + f"HW_ARCH {arch.group(1)} runs {first.group(1)}, any other {rest.group(1)}, " + f"from {game.group(1).rstrip('/')}/${{HW_ARCH}} when that folder exists" + ), + ) + ] + + LINK_SCRIPTS = ( ("drastic", f"{STANDALONE}/drastic-sa/scripts/start_drastic.sh"), ("dolphin", f"{STANDALONE}/dolphin-sa/scripts/start_dolphin_gc.sh"), @@ -679,6 +720,7 @@ EXTRACTORS: tuple[Callable[[RepoTree], list[Launcher]], ...] = ( yabasanshiro, duckstation, armsx2, + pico8, patched_directories, links, ) diff --git a/tests/test_launchers_retrobat.py b/tests/test_launchers_retrobat.py index 3f33dd13b..8cb1d98b4 100644 --- a/tests/test_launchers_retrobat.py +++ b/tests/test_launchers_retrobat.py @@ -1141,6 +1141,36 @@ class FolderArguments(unittest.TestCase): ) +PICO8 = """ + public override System.Diagnostics.ProcessStartInfo Generate(string system, string emulator, string core, string rom, string playersControllers, ScreenResolution resolution) + { + string path = AppConfig.GetFullPath("pico8"); + + string exe = Path.Combine(path, "pico8.exe"); + if (!File.Exists(exe)) + return null; + + return new ProcessStartInfo() + { + FileName = exe, + WorkingDirectory = path, + Arguments = args, + }; + } +""" + + +class ProgramsRunFromEmulatorFolders(unittest.TestCase): + def test_pico8_runs_its_binary_from_its_emulator_folder(self): + (record,) = rl.pico8(_sources(**{f"{G}/Pico8.Generator.cs": PICO8})) + self.assertEqual( + (record["emulator"], record["kind"], record["names"], record["search_dir"], + record["passes_to"], record["source_ref"]), + ("pico8", "fixed_name", ["pico8.exe"], '', "exec", + f"{G}/Pico8.Generator.cs:3-7, 11-12"), + ) + + class EveryBiosReferenceIsRead(unittest.TestCase): def test_unread_reference_stops_the_scrape(self): extra = EKA2L1 + ( diff --git a/tests/test_launchers_retropie.py b/tests/test_launchers_retropie.py index 0e5ef9c66..510aeaae7 100644 --- a/tests/test_launchers_retropie.py +++ b/tests/test_launchers_retropie.py @@ -293,6 +293,28 @@ DRASTIC = module("drastic", """ """, 36) +UAE4ALL = module("uae4all", """ + function configure_uae4all() { + mkRomDir "amiga" + + moveConfigDir "$md_inst/kickstarts" "$biosdir/amiga" + + rm -f "$romdir/amiga/+Start UAE4All.sh" + if [[ "$md_mode" == "install" ]]; then + cat > "$romdir/amiga/+Start UAE4All.sh" << _EOF_ + #!/bin/bash + pushd "$md_inst" + ./uae4all + popd + _EOF_ + fi + + addEmulator 1 "$md_id" "amiga" "bash $romdir/amiga/+Start\\ UAE4All.sh" + addSystem "amiga" + } +""", 60) + + class WorkingDirectory(unittest.TestCase): def test_drastic_reads_below_the_folder_its_script_enters(self): found = records({f"{EMULATORS}/drastic.sh": DRASTIC}, "drastic") @@ -306,6 +328,18 @@ class WorkingDirectory(unittest.TestCase): )], ) + def test_uae4all_runs_from_its_install_directory(self): + found = records({f"{EMULATORS}/uae4all.sh": UAE4ALL}, "uae4all") + entered = [r for r in found if r["kind"] == "passes_path"] + self.assertEqual( + [(r["search_dir"], r["passes_to"], r["source_ref"]) for r in entered], + [( + "$md_inst", + "working directory of $romdir/amiga/+Start UAE4All.sh", + f"{EMULATORS}/uae4all.sh:69", + )], + ) + def test_a_launch_script_that_no_longer_enters_a_folder_raises(self): files = {f"{EMULATORS}/drastic.sh": DRASTIC.replace('pushd "', 'echo "')} with self.assertRaises(rl.LauncherPatternMissing): diff --git a/tests/test_launchers_rocknix.py b/tests/test_launchers_rocknix.py index 22596256b..26fa9192c 100644 --- a/tests/test_launchers_rocknix.py +++ b/tests/test_launchers_rocknix.py @@ -551,6 +551,53 @@ class Links(unittest.TestCase): rl.links(tree) +START_PICO8 = text( + """ + #!/bin/bash + . /etc/profile + + GAME_DIR="/storage/roms/pico-8/" + + case ${HW_ARCH} in + aarch64) + STATIC_BIN="pico8_64" + ;; + *) + STATIC_BIN="pico8_dyn" + ;; + esac + + if [ -d "${GAME_DIR}/${HW_ARCH}" ] + then + LAUNCH_DIR="${GAME_DIR}/${HW_ARCH}" + else + LAUNCH_DIR="${GAME_DIR}" + fi + + ${LAUNCH_DIR}/${STATIC_BIN} -home -root_path ${GAME_DIR} -joystick 0 ${OPTIONS} "${CART}" +""" +) + + +class ProgramsRunOutsideTheBiosFolder(unittest.TestCase): + PATH = f"{SA}/pico-8/scripts/start_pico8.sh" + + def test_pico8_runs_its_binary_from_roms(self): + (record,) = [r.entry() for r in rl.pico8(RepoTree.from_files({self.PATH: START_PICO8}))] + self.assertEqual( + (record["emulator"], record["kind"], record["names"], record["search_dir"], + record["passes_to"]), + ("pico-8", "fixed_name", ["pico8_64", "pico8_dyn"], "/storage/roms/pico-8", "exec"), + ) + self.assertIn("HW_ARCH aarch64 runs pico8_64", record["depends_on_option"]) + self.assertEqual(record["source_ref"], f"{self.PATH}:4-11, 15-22") + + def test_a_script_without_its_binary_choice_raises(self): + broken = START_PICO8.replace('STATIC_BIN="pico8_dyn"', "STATIC_BIN=$OTHER") + with self.assertRaises(LauncherPatternMissing): + rl.pico8(RepoTree.from_files({self.PATH: broken})) + + class Revision(unittest.TestCase): def test_the_revision_is_the_one_the_bios_list_is_read_at(self): url = (