diff --git a/platforms/knulli.yml b/platforms/knulli.yml index 1ca669064..5d443be22 100644 --- a/platforms/knulli.yml +++ b/platforms/knulli.yml @@ -5793,29 +5793,32 @@ launchers: - switch/prod.keys case_insensitive: false search_dir: /userdata/bios - passes_to: copied to /userdata/system/configs/Ryujinx/system/prod.keys + passes_to: /userdata/system/configs/Ryujinx/system/prod.keys fails_when_missing: false + depends_on_option: "ryujinxKeys.exists()" source_ref: "package/system/knulli-configgen/configgen/configgen/generators/ryujinx/ryujinxGenerator.py:107-109" - emulator: pcsx2 build: standalone kind: copies names: - - ps2/patches.zip + - patches.zip case_insensitive: false - search_dir: /userdata/bios - passes_to: copied from /usr/share/knulli/datainit/bios/ps2/patches.zip + search_dir: /usr/share/knulli/datainit/bios/ps2 + passes_to: /userdata/bios/ps2/patches.zip fails_when_missing: false + depends_on_option: "not pcsx2Patches.exists()" source_ref: "package/system/knulli-configgen/configgen/configgen/generators/pcsx2/pcsx2Generator.py:78,\ \ 121-122" - emulator: aethersx2 build: standalone kind: copies names: - - ps2/patches.zip + - patches.zip case_insensitive: false - search_dir: /userdata/bios - passes_to: copied from /usr/share/knulli/datainit/bios/ps2/patches.zip + search_dir: /usr/share/knulli/datainit/bios/ps2 + passes_to: /userdata/bios/ps2/patches.zip fails_when_missing: false + depends_on_option: "not pcsx2Patches.exists()" source_ref: "package/system/knulli-configgen/configgen/configgen/generators/aethersx2/aethersx2Generator.py:59,\ \ 93-94" - emulator: gsplus diff --git a/platforms/retrobat.yml b/platforms/retrobat.yml index 7cbb89ea0..736923af7 100644 --- a/platforms/retrobat.yml +++ b/platforms/retrobat.yml @@ -3880,9 +3880,10 @@ launchers: - capsimg.dll case_insensitive: true search_dir: bios - passes_to: copied to /, only when the target does not exist + passes_to: / fails_when_missing: false - depends_on_option: core in capsimgCore, rom extension ".ipf" + depends_on_option: core in capsimgCore, rom extension ".ipf", the target does not + exist source_ref: "emulatorLauncher/Generators/LibRetro.Generator.cs:1018-1027, 2750" - emulator: demul build: standalone @@ -3943,11 +3944,11 @@ launchers: - segaboot.gcm case_insensitive: true search_dir: bios - passes_to: first found copied to /User/Triforce/segaboot.gcm, skipped - when the target is as large and not older + passes_to: /User/Triforce/segaboot.gcm fails_when_missing: "warning '[WARNING] Triforce: segaboot.gcm not found in bios\\\ triforce, test menu will not be available.' when the target is absent too" - depends_on_option: _triforce, !_crediar + depends_on_option: _triforce, !_crediar, first name found, the target is absent, + smaller or older source_ref: "emulatorLauncher/Generators/Dolphin.Generator.cs:782-783, 1048-1083" - emulator: dosbox-pure build: standalone @@ -3959,8 +3960,9 @@ launchers: - Roland_SC-88.sf2 case_insensitive: true search_dir: bios/scummvm/extra - passes_to: copied to /system/, only when the target does not exist + passes_to: /system/ fails_when_missing: false + depends_on_option: the target does not exist source_ref: "emulatorLauncher/Generators/DosBoxPure.Generator.cs:125, 130-148" - emulator: dosbox-staging build: standalone @@ -3970,9 +3972,9 @@ launchers: - MT32_PCM.ROM case_insensitive: true search_dir: bios/scummvm/extra - passes_to: copied to /mt32-roms/, only when the target does not - exist + passes_to: /mt32-roms/ fails_when_missing: false + depends_on_option: the target does not exist source_ref: "emulatorLauncher/Generators/DosBoxStaging.Generator.cs:321, 326-334" - emulator: dosbox-staging build: standalone @@ -3982,9 +3984,9 @@ launchers: - Roland_SC-88.sf2 case_insensitive: true search_dir: bios/scummvm/extra - passes_to: copied to /soundfonts/, only when the target does not - exist + passes_to: /soundfonts/ fails_when_missing: false + depends_on_option: the target does not exist source_ref: "emulatorLauncher/Generators/DosBoxStaging.Generator.cs:318, 336-344" - emulator: duckstation build: standalone @@ -4426,9 +4428,9 @@ launchers: names: *id001 case_insensitive: true search_dir: bios/hatari/tos - passes_to: copied to bios/hatarib/, only when the target does not exist + passes_to: bios/hatarib/ fails_when_missing: false - depends_on_option: hatarib_tos + depends_on_option: hatarib_tos, the target does not exist source_ref: "emulatorLauncher/Generators/LibRetro.CoreOptions.cs:3434-3435, 3457-3460" - emulator: jzintv build: standalone @@ -4620,9 +4622,9 @@ launchers: - syscard3.pce case_insensitive: true search_dir: bios - passes_to: "copied to /Firmware/[BIOS] Super CD-ROM System (Japan) (v3.0).pce,\ - \ only when the target does not exist" + passes_to: "/Firmware/[BIOS] Super CD-ROM System (Japan) (v3.0).pce" fails_when_missing: "ApplicationException 'PCE CD firmware is missing (syscard3.pce)'" + depends_on_option: the target does not exist source_ref: "emulatorLauncher/Generators/Mesen.Generator.cs:254-269" - emulator: mesen build: standalone @@ -4632,10 +4634,10 @@ launchers: - SGB2.sfc case_insensitive: true search_dir: bios - passes_to: copied to /Firmware/SGB1.sfc, SGB2.sfc, only when the target does - not exist + passes_to: /Firmware/SGB1.sfc, /Firmware/SGB2.sfc fails_when_missing: "ApplicationException 'Super Gameboy firmware is missing (SGB1.sfc\ \ and/or SGB2.sfc)'" + depends_on_option: the target does not exist source_ref: "emulatorLauncher/Generators/Mesen.Generator.cs:281-304" - emulator: mesen build: standalone @@ -4644,9 +4646,9 @@ launchers: - gba_bios.bin case_insensitive: true search_dir: bios - passes_to: copied to /Firmware/gba_bios.bin, only when the target does not - exist + passes_to: /Firmware/gba_bios.bin fails_when_missing: false + depends_on_option: the target does not exist source_ref: "emulatorLauncher/Generators/Mesen.Generator.cs:307-324" - emulator: mesen build: standalone @@ -4655,8 +4657,9 @@ launchers: - colecovision.rom case_insensitive: true search_dir: bios - passes_to: copied to /Firmware/bios.col, only when the target does not exist + passes_to: /Firmware/bios.col fails_when_missing: "ApplicationException 'Colecovision firmware is missing (colecovision.rom)'" + depends_on_option: the target does not exist source_ref: "emulatorLauncher/Generators/Mesen.Generator.cs:326-346" - emulator: openmsx build: standalone @@ -5269,9 +5272,9 @@ launchers: - kick40068.A4000 case_insensitive: true search_dir: bios, bios/amiga, bios/Amiga - passes_to: copied to /Devs/Kickstarts/ from the system's Kickstart - candidates + passes_to: /Devs/Kickstarts/ fails_when_missing: false + depends_on_option: the system's Kickstart candidates source_ref: "emulatorLauncher/Generators/Uae.Generator.cs:1002-1011, 1026-1032" - emulator: xemu build: standalone @@ -5343,9 +5346,10 @@ launchers: - IPLROM30.DAT case_insensitive: true search_dir: bios/keropi - passes_to: copied to /, only when the target does not exist + passes_to: / fails_when_missing: false - depends_on_option: "!string.IsNullOrEmpty(biosRoot), Directory.Exists(biosPath)" + depends_on_option: "!string.IsNullOrEmpty(biosRoot), Directory.Exists(biosPath),\ + \ the target does not exist" source_ref: "emulatorLauncher/Generators/Xm6pro.Generator.cs:118-141" - emulator: xroar build: standalone diff --git a/schemas/platform.schema.json b/schemas/platform.schema.json index 28786b8ca..0d409de16 100644 --- a/schemas/platform.schema.json +++ b/schemas/platform.schema.json @@ -226,7 +226,7 @@ "scans_dir", "package" ], - "description": "names_searched: tries the names in order, first found wins. fixed_name: writes or opens one path per name. core_option: sets a core option whose value names the file. passes_path: hands over a directory. links, copies: links or copies a file to where the emulator reads it. moves: moves a file the user left in one folder to the folder the launcher links from. scans_dir: the emulator picks from a directory. package: names no file, records what a package builds (`repo`) and the systems it registers (`systems`)" + "description": "names_searched: tries the names in order, first found wins. fixed_name: writes or opens one path per name. core_option: sets a core option whose value names the file. passes_path: hands over a directory. links, copies: links or copies a file to where the emulator reads it (a copy names its files under search_dir, the folder it copies from, passes_to is the target, and the case that skips it is in depends_on_option). moves: moves a file the user left in one folder to the folder the launcher links from. scans_dir: the emulator picks from a directory. package: names no file, records what a package builds (`repo`) and the systems it registers (`systems`)" }, "package": { "type": "string", diff --git a/scripts/scraper/knulli_launchers.py b/scripts/scraper/knulli_launchers.py index 8ebc0fd59..219174230 100644 --- a/scripts/scraper/knulli_launchers.py +++ b/scripts/scraper/knulli_launchers.py @@ -332,7 +332,8 @@ class GenericSites: """A generator module whose BIOS path uses bios_sites() reads. kind is fixed_name when the path names a file, passes_path when it names - a directory. A copy is recorded as `copies` whatever the kind. caller + a directory. A copy is recorded as `copies` whatever the kind, its names + read from the folder the code copies from and passes_to the target. caller names the module and function whose core test selects the cores this helper module runs for, emulator the name when no core test does. """ @@ -384,6 +385,8 @@ class _Site: conditions: list[str] cores: list[str] expands: dict[str, list[str]] = field(default_factory=dict) + # A copy reads its file from `source` and hands it to `passes_to`. + source: str | None = None class _Reader: @@ -394,6 +397,7 @@ class _Reader: self.src = src self.bios = cg.bios.rstrip("/") self.parents: dict[int, ast.AST] = {} + self.copy_sources: dict[int, str] = {} for parent in ast.walk(src.tree): for child in ast.iter_child_nodes(parent): self.parents[id(child)] = parent @@ -523,10 +527,14 @@ class _Reader: continue yield node, value # type: ignore[misc] - def conditions(self, node: ast.AST) -> tuple[list[str], list[tuple[int, int]]]: + def conditions( + self, node: ast.AST, guards: bool = False + ) -> tuple[list[str], list[tuple[int, int]]]: """Tests of the ifs a node sits in, outermost first, and their lines. - A presence test of a BIOS path is a guard, kept for its lines only. + A presence test of a BIOS path is a guard, kept for its lines only, + unless guards is set: a copy made only when its source or its target + is there carries the test as a condition. """ tests: list[str] = [] spans: list[tuple[int, int]] = [] @@ -535,7 +543,7 @@ class _Reader: if isinstance(parent, ast.If) and child is not parent.test: spans.append(self.src.span(parent.test)) text = ast.unparse(parent.test) - if not self._is_guard(parent.test): + if guards or not self._is_guard(parent.test): in_else = any(child is stmt for stmt in parent.orelse) tests.append(f"not ({text})" if in_else else text) child, parent = parent, self.parents.get(id(parent)) @@ -660,9 +668,10 @@ class _Reader: receiver = func.value if isinstance(func, ast.Attribute) else None if attr in ("copy", "copyfile", "copy2") and receiver is not None and ast.unparse(receiver) == "shutil": source, target = parent.args[0], parent.args[1] - if source is node: - return f"copied to {self.resolve(target) or ast.unparse(target)}", "copies" - return f"copied from {self.resolve(source) or ast.unparse(source)}", "copies" + self.copy_sources[id(node)] = value if source is node else ( + self.resolve(source) or ast.unparse(source) + ) + return (value if target is node else self.resolve(target) or ast.unparse(target)), "copies" if ( attr == "append" and isinstance(receiver, ast.Name) @@ -718,7 +727,7 @@ class _Reader: def _site( self, read: ast.AST, value: str, passes_to: str, kind: str | None, defined: ast.AST ) -> _Site: - tests, spans = self.conditions(read) + tests, spans = self.conditions(read, guards=kind == "copies") lines = [self.src.span(self.statement_of(read)), *spans] if defined is not read: # A path bound in one branch and read after the if: the branch @@ -726,7 +735,10 @@ class _Reader: bound_tests, bound_spans = self.conditions(defined) tests = list(dict.fromkeys([*bound_tests, *tests])) lines += [self.src.span(self.statement_of(defined)), *bound_spans] - return _Site(value, passes_to, kind, lines, tests, self.cores(read), self._expands(read)) + return _Site( + value, passes_to, kind, lines, tests, self.cores(read), self._expands(read), + self.copy_sources.get(id(read)), + ) def _guard_site(self, read: ast.AST, value: str) -> _Site: """A presence test whose if body is the only consumer of the path.""" @@ -802,7 +814,12 @@ def bios_sites(cg: Configgen, spec: GenericSites) -> list[Launcher]: expands = {k: v for site in group for k, v in site.expands.items()} names: list[str] = [] search_dir = path - if kind in ("fixed_name", "copies"): + source = group[0].source + if kind == "copies" and source is not None and not source.startswith(prefix): + # Copied into BIOS: the file is read from the folder it comes from. + search_dir, _, name = source.rpartition("/") + names = [name] + elif kind in ("fixed_name", "copies"): if not path.startswith(prefix): src.fail(f"{path} names no file under {cg.bios}") names, search_dir = [path[len(prefix):]], cg.bios diff --git a/scripts/scraper/retrobat_launchers.py b/scripts/scraper/retrobat_launchers.py index 6ef1898a5..a3957f340 100644 --- a/scripts/scraper/retrobat_launchers.py +++ b/scripts/scraper/retrobat_launchers.py @@ -320,6 +320,16 @@ def feature_choices( return values, src.lines(feature.start(), close + len("")) +# A copy record reads `names` from `search_dir` and hands them to the path in +# `passes_to`. When the copy is skipped is a condition of the record. +TARGET_ABSENT = "the target does not exist" + + +def _joined(*parts: str | None) -> str | None: + """Conditions of a record, joined, None when there are none.""" + return ", ".join(part for part in parts if part) or None + + def _record( emulator: str, build: str, kind: str, names: list[str], ref: str, **fields: object ) -> Record: @@ -611,9 +621,9 @@ def hatari_libretro(sources: dict[str, Source]) -> list[Record]: source_ref((src.path, [head, src.lines(shared.start(), copy.end())])), case_insensitive=True, search_dir="/".join(["bios", *folders]), - passes_to=f"copied to bios/{own.group(2)}/, only when the target does not exist", + passes_to=f"bios/{own.group(2)}/", fails_when_missing=False, - depends_on_option=key, + depends_on_option=_joined(key, TARGET_ABSENT), ) ) return records @@ -661,10 +671,9 @@ def mesen(sources: dict[str, Source]) -> list[Record]: source_ref((src.path, [src.statement_span(block)])), case_insensitive=True, search_dir="bios", - passes_to="copied to /Firmware/" - + ", ".join(destinations) - + ", only when the target does not exist", + passes_to=", ".join(f"/Firmware/{d}" for d in destinations), fails_when_missing=fails, + depends_on_option=TARGET_ABSENT, ) ) return records @@ -804,11 +813,9 @@ def winuae(sources: dict[str, Source]) -> list[Record]: ), case_insensitive=True, search_dir=search, - passes_to=( - f"copied to /{'/'.join(_strings(folder.group(1)))}/" - " from the system's Kickstart candidates" - ), + passes_to=f"/{'/'.join(_strings(folder.group(1)))}/", fails_when_missing=False, + depends_on_option="the system's Kickstart candidates", ) ) return records @@ -2480,10 +2487,12 @@ def dolphin(sources: dict[str, Source]) -> list[Record]: ), case_insensitive=True, search_dir="bios", - passes_to=f"first found copied to {destination[0] if destination else target.group(1)}," - " skipped when the target is as large and not older", + passes_to=destination[0] if destination else target.group(1), fails_when_missing=f"warning '{_unescape(warning.group(1))}' when the target is absent too", - depends_on_option=", ".join(conditions) or None, + depends_on_option=_joined( + *conditions, "first name found", + "the target is absent, smaller or older", + ), ) ) return _covered(src, records, skip=(r"DolphinTool\.exe",)) @@ -2578,9 +2587,9 @@ def _copies_if_absent( _path_parts(src, *returns, *[s for c in copies for s in c[3]]), case_insensitive=True, search_dir=folders.pop(), - passes_to=f"copied to {destination}/, only when the target does not exist", + passes_to=f"{destination}/", fails_when_missing=False, - depends_on_option=", ".join(conditions) or None, + depends_on_option=_joined(*conditions, TARGET_ABSENT), ) ) if not records: @@ -2619,7 +2628,9 @@ def capsimg(sources: dict[str, Source]) -> list[Record]: record["source_ref"] = source_ref( (src.path, [(src.line(cores.start()), core_lines[1]), src.statement_span(block)]) ) - record["depends_on_option"] = f'core in {cores.group(1)}, rom extension "{gate.group(1)}"' + record["depends_on_option"] = _joined( + f'core in {cores.group(1)}, rom extension "{gate.group(1)}"', TARGET_ABSENT + ) return [record] @@ -3108,9 +3119,9 @@ def xm6pro(sources: dict[str, Source]) -> list[Record]: _path_parts(src, (src.line(table.start()), lines[1]), src.lines(method.start(), body[1])), case_insensitive=True, search_dir=folder[0], - passes_to="copied to /, only when the target does not exist", + passes_to="/", fails_when_missing=False, - depends_on_option=", ".join(conditions) or None, + depends_on_option=_joined(*conditions, TARGET_ABSENT), ) return _covered(src, [record]) diff --git a/tests/test_launchers_knulli.py b/tests/test_launchers_knulli.py index 8ea3070b0..ca0b6e7da 100644 --- a/tests/test_launchers_knulli.py +++ b/tests/test_launchers_knulli.py @@ -451,8 +451,9 @@ class BiosSites(unittest.TestCase): (record,) = sites("ryujinx/ryujinxGenerator.py", "fixed_name") self.assertEqual(record["kind"], "copies") self.assertEqual(record["names"], ["switch/prod.keys"]) - self.assertEqual(record["passes_to"], "copied to /userdata/system/configs/Ryujinx/system/prod.keys") - self.assertNotIn("depends_on_option", record) + self.assertEqual(record["search_dir"], "/userdata/bios") + self.assertEqual(record["passes_to"], "/userdata/system/configs/Ryujinx/system/prod.keys") + self.assertEqual(record["depends_on_option"], "ryujinxKeys.exists()") def test_a_presence_test_over_a_literal_list(self): (record,) = sites("dolphin/dolphinGenerator.py", "fixed_name") diff --git a/tests/test_launchers_retrobat.py b/tests/test_launchers_retrobat.py index 3f33dd13b..23029453f 100644 --- a/tests/test_launchers_retrobat.py +++ b/tests/test_launchers_retrobat.py @@ -1042,7 +1042,10 @@ class HelpersAndCopies(unittest.TestCase): ) self.assertEqual(segaboot["kind"], "copies") self.assertEqual(segaboot["names"], ["triforce/segaboot.gcm", "segaboot.gcm"]) - self.assertEqual(segaboot["depends_on_option"], "_triforce, !_crediar") + self.assertEqual( + segaboot["depends_on_option"], + "_triforce, !_crediar, first name found, the target is absent, smaller or older", + ) def test_dosbox_copies_grouped_by_target_folder(self): sources = _sources( @@ -1055,16 +1058,17 @@ class HelpersAndCopies(unittest.TestCase): ( "dosbox-pure", ["MT32_CONTROL.ROM"], - "copied to /mt32-roms/, only when the target does not exist", + "/mt32-roms/", ), ( "dosbox-pure", ["Roland_SC-55.sf2"], - "copied to /soundfonts/, only when the target does not exist", + "/soundfonts/", ), ], ) self.assertEqual(records[0]["search_dir"], "bios/scummvm/extra") + self.assertEqual(records[0]["depends_on_option"], "the target does not exist") def test_tsugaru_folder_chain(self): [record] = rl.tsugaru(_sources(**{f"{G}/Tsugaru.Generator.cs": TSUGARU})) diff --git a/wiki/adding-a-platform.md b/wiki/adding-a-platform.md index 3e39160d4..4a426b86c 100644 --- a/wiki/adding-a-platform.md +++ b/wiki/adding-a-platform.md @@ -491,7 +491,8 @@ the launcher's name, a file the core accepts: the file the collection knows under that name when the core takes its content, otherwise the copy held for the core entry read under that name. Records that only hand over a directory (`passes_path`, `scans_dir`, a `links` record with no names) or copy a file -the platform already names (`copies`) place nothing, and a `search_dir` the +the platform already names (`copies`, its `names` under the `search_dir` it +copies from, the target in `passes_to`) place nothing, and a `search_dir` the code describes rather than names ("directory of the Chihiro BIOS found") is skipped. `$biosdir`, `${biosPath}` and `$bios_path` are the BIOS directory as RetroPie, EmuDeck and RetroDECK spell it, and a path from a root