From 4b17bf04606c726ed52d5c4f71d1d9bc67c6c662 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Mon, 9 Nov 2026 20:15:47 +0100 Subject: [PATCH] fix: read xroar coco3 ROMs from RetroBat coco RetroBat passes XRoar -rompath bios/coco for the coco system, and its es_features offers the CoCo 3 machines there. The per-system record now carries those machines (machines:), and an entry whose name the platform list does not declare is launched for the system offering its own profile system as a machine, by _norm_system_id. coco3.rom and coco3p.rom move from bios/dragon to bios/coco. RetroBat offers no MC-10 machine, so the mc10 ROMs keep the default bios/dragon. --- platforms/retrobat.yml | 11 ++++++- schemas/platform.schema.json | 7 +++++ scripts/common.py | 26 ++++++++++++++-- scripts/scraper/configgen_launchers.py | 4 ++- scripts/scraper/retrobat_launchers.py | 42 ++++++++++++++++++++++++++ tests/test_launchers_retrobat.py | 27 +++++++++++++++-- tests/test_standalone_links.py | 28 +++++++++++++++-- wiki/adding-a-platform.md | 1 + 8 files changed, 138 insertions(+), 8 deletions(-) diff --git a/platforms/retrobat.yml b/platforms/retrobat.yml index c6862a448..de95d06ba 100644 --- a/platforms/retrobat.yml +++ b/platforms/retrobat.yml @@ -5402,7 +5402,16 @@ launchers: fails_when_missing: false systems: - coco - source_ref: "emulatorLauncher/Generators/Xroar.Generator.cs:76-80" + machines: + - coco + - cocous + - coco2b + - coco2bus + - coco3 + - coco3h + - coco3p + - coco3ph + source_ref: "emulatorLauncher/Generators/Xroar.Generator.cs:76-80, .emulationstation/es_features.cfg:22650-22659" - emulator: xroar build: standalone kind: passes_path diff --git a/schemas/platform.schema.json b/schemas/platform.schema.json index 9e63e0dba..5c0b0c1b1 100644 --- a/schemas/platform.schema.json +++ b/schemas/platform.schema.json @@ -243,6 +243,13 @@ }, "description": "Systems the package registers, under the platform's own names, in the order the script adds them. On a record handing a directory, the systems it is written for: the record applies to an emulator launched for one of them, and replaces there what the records naming no system hand" }, + "machines": { + "type": "array", + "items": { + "type": "string" + }, + "description": "On a record naming systems, the machines the platform offers the emulator under them, as the emulator names them: an entry of a profile system spelled like one of them is launched for those systems" + }, "slot": { "type": "string", "description": "The key the launcher files this list under, such as a region or a machine" diff --git a/scripts/common.py b/scripts/common.py index aece6b932..27fd2b30c 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -2697,6 +2697,8 @@ EXECUTED_PREFIX = "exec:" # systems a launcher record is written for, as JSON [system, name]: it says # which launch reads a profile entry of that name. SYSTEM_FILE_PREFIX = "sysfile:" +# The key, in that index, of a machine a launcher record offers. +MACHINE_KEY = "machine:" def core_system_dirs( @@ -2781,6 +2783,18 @@ def standalone_set(config: dict) -> set[str]: for f in system.get("files") or [] if f.get("name") } + # The machines a record offers under its systems, by normalized id. + from launchers import native_systems # noqa: PLC0415 + + natives = native_systems(config) + names |= { + SYSTEM_FILE_PREFIX + json.dumps( + [natives.get(str(native), str(native)), MACHINE_KEY + _norm_system_id(str(m))] + ) + for record in config.get("launchers") or [] + for native in record.get("systems") or [] + for m in record.get("machines") or [] + } names |= { EXECUTED_PREFIX + json.dumps([run.emulator, run.name, run.place, run.source_ref]) for run in executed_files(config) @@ -3000,9 +3014,17 @@ def platform_layouts( def bindings_of(entry: dict) -> dict[str, tuple[str, str]]: systems = frozenset() if per_system: + own = entry_launch_systems(entry, {}) systems = entry_launch_systems(entry, index) - if systems == entry_launch_systems(entry, {}): - systems = systems.union(*(family.get(s, set()) for s in systems)) + if systems == own: + systems = systems.union(*(family.get(s, set()) for s in own)) + if systems == own: + # The platform's list names none of the entry's names: its + # own system selects the launch that offers it as a machine + # (RetroBat runs XRoar's coco3 under coco). + systems = systems.union(*( + index.get(MACHINE_KEY + _norm_system_id(s), set()) for s in own + )) if systems not in by_systems: by_systems[systems] = standalone_bindings_for( emu_name, profile, standalone_cores, systems diff --git a/scripts/scraper/configgen_launchers.py b/scripts/scraper/configgen_launchers.py index fe5214088..5c9e3a70c 100644 --- a/scripts/scraper/configgen_launchers.py +++ b/scripts/scraper/configgen_launchers.py @@ -35,6 +35,8 @@ class Launcher: fails_scope: str | None = None depends_on_option: str | None = None device: str | None = None + # The machines the platform offers under the record's systems. + machines: list[str] | None = None def entry(self) -> dict[str, Any]: """The platform YAML form, in a fixed key order, unset keys left out.""" @@ -42,7 +44,7 @@ class Launcher: "emulator", "build", "kind", "slot", "names", "expands", "case_insensitive", "search_dir", "passes_to", "fails_when_missing", "fails_scope", "depends_on_option", - "device", "source_ref", + "device", "machines", "source_ref", ) return {key: getattr(self, key) for key in order if getattr(self, key) is not None} diff --git a/scripts/scraper/retrobat_launchers.py b/scripts/scraper/retrobat_launchers.py index 4b8b393ea..8d624ceaa 100644 --- a/scripts/scraper/retrobat_launchers.py +++ b/scripts/scraper/retrobat_launchers.py @@ -292,6 +292,33 @@ def _throw_message(src: Source, start: int, end: int) -> tuple[str, int]: return f"ApplicationException '{message}'", src.line(found.start()) +def system_feature_choices( + src: Source, emulator: str, option: str, system: str +) -> tuple[list[str], tuple[int, int]]: + """Values es_features.cfg offers for an option of an emulator under one system.""" + text = src.text + for found in re.finditer(r'", found.start()) + break + else: + raise LauncherPatternMissing(src.path, f"emulator {emulator}") + for block in re.finditer(r'', text[start:end]): + if system not in [name.strip() for name in block.group(1).split(",")]: + continue + scope = start + block.start() + close = text.find("", scope) + feature = src.find( + rf']*value="{re.escape(option)}"', f"{option} of {system}", scope, close + ) + stop = text.find("", feature.end()) + values = re.findall(r']*value="([^"]*)"', text[feature.end():stop]) + if not values: + raise LauncherPatternMissing(src.path, f"choices of {option} for {system}") + return values, src.lines(feature.start(), stop + len("")) + raise LauncherPatternMissing(src.path, f"system {system} of {emulator}") + + def feature_choices( src: Source, emulator: str, option: str, core: str | None = None ) -> tuple[list[str], tuple[int, int]]: @@ -2983,6 +3010,21 @@ def xroar(sources: dict[str, Source]) -> list[Record]: for record, conditions in found for split in _per_system(src, _with_conditions(record, conditions)) ] + # The machines ES offers under a system the folder is chosen for: XRoar + # runs a CoCo 3 for coco, so its ROMs are read from that system's folder. + features = sources[FEATURES] + for record in records: + if not record.get("systems"): + continue + machines: list[str] = [] + lines = [] + for system in record["systems"]: + choices, span = system_feature_choices(features, "xroar", "xroar_machine", system) + machines += [m for m in choices if m not in machines] + lines.append(span) + record["machines"] = machines + ref = record.pop("source_ref") + record["source_ref"] = f"{ref}, {source_ref((features.path, lines))}" line = src.find(r'string (\w+) = ("rompath (?:[^"\\\n]|\\.)*");', "config rompath line") written = _unescape(line.group(2)[1:-1]) relative = re.fullmatch(r'rompath "(.*)"', written) diff --git a/tests/test_launchers_retrobat.py b/tests/test_launchers_retrobat.py index da7832502..ca3db8e35 100644 --- a/tests/test_launchers_retrobat.py +++ b/tests/test_launchers_retrobat.py @@ -1150,7 +1150,9 @@ class FolderArguments(unittest.TestCase): def test_xroar_reassigned_folder_and_config_line(self): # romPath is bios/dragon, rewritten to bios/coco for the coco system. - default, coco, config = rl.xroar(_sources(**{f"{G}/Xroar.Generator.cs": XROAR})) + default, coco, config = rl.xroar(_sources(**{ + f"{G}/Xroar.Generator.cs": XROAR, rl.FEATURES: XROAR_FEATURES, + })) self.assertEqual( (default["search_dir"], default["passes_to"], default.get("systems")), ("bios/dragon", "command line -rompath bios/dragon", None), @@ -1159,13 +1161,34 @@ class FolderArguments(unittest.TestCase): (coco["search_dir"], coco["passes_to"], coco["systems"]), ("bios/coco", "command line -rompath bios/coco", ["coco"]), ) - self.assertEqual(coco["source_ref"], default["source_ref"]) + self.assertEqual(coco["machines"], ["coco", "coco3"]) + self.assertNotIn("machines", default) + self.assertEqual( + coco["source_ref"], f"{default['source_ref']}, {rl.FEATURES}:3-6" + ) self.assertEqual(config["search_dir"], "bios/dragon") self.assertEqual( config["passes_to"], 'xroar.conf rompath ".\\\\..\\\\..\\\\bios\\\\dragon"' ) +XROAR_FEATURES = """ + + + + + + + + + + + + + +""" + + PICO8 = """ public override System.Diagnostics.ProcessStartInfo Generate(string system, string emulator, string core, string rom, string playersControllers, ScreenResolution resolution) { diff --git a/tests/test_standalone_links.py b/tests/test_standalone_links.py index 2557677c6..1fcd5eedd 100644 --- a/tests/test_standalone_links.py +++ b/tests/test_standalone_links.py @@ -308,6 +308,29 @@ class SystemFamily(unittest.TestCase): ) +class OfferedMachine(unittest.TestCase): + def test_an_entry_system_offered_as_a_machine_selects_that_launch(self) -> None: + profile = { + "emulator": "Emu", "type": "standalone", "standalone_pools": {"-rompath": ""}, + "files": [ + {"name": "coco3.rom", "path": "coco3.rom", "system": "coco3"}, + {"name": "mc10.rom", "path": "mc10.rom", "system": "mc10"}, + ], + } + cfg = { + **config( + passes("emu", "bios/dragon", "-rompath"), + {**passes("emu", "bios/coco", "-rompath"), "systems": ["coco"], + "machines": ["coco", "coco3"]}, + ), + "systems": {"coco": {"files": [{"name": "x.zip", "native_system": "coco"}]}}, + } + ((view, _standalone),) = platform_layouts("emu", profile, standalone_set(cfg)) + self.assertEqual( + [f.get("standalone_path") for f in view["files"]], ["coco/coco3.rom", "dragon/mc10.rom"] + ) + + class Layout(unittest.TestCase): LINKED = link("emu", "$biosdir/dc", "$home/.local/share/emu is a symlink to it") @@ -574,8 +597,9 @@ class XroarOnEachPlatform(unittest.TestCase): "batocera": dict.fromkeys(("dragon32", "dragon64", "coco", "coco3", "mc10"), "xroar"), "retrodeck": dict.fromkeys(("dragon32", "dragon64", "coco", "coco3", "mc10"), ""), "retropie": dict.fromkeys(("dragon32", "dragon64", "coco", "coco3", "mc10"), ""), - "retrobat": {**dict.fromkeys(("dragon32", "dragon64", "coco3", "mc10"), "dragon"), - "coco": "coco"}, + # RetroBat offers XRoar's CoCo 3 machines under coco and no MC-10. + "retrobat": {**dict.fromkeys(("dragon32", "dragon64", "mc10"), "dragon"), + **dict.fromkeys(("coco", "coco3"), "coco")}, "recalbox": {**dict.fromkeys(("dragon32", "dragon64"), "dragon"), **dict.fromkeys(("coco", "coco3", "mc10"), "trs80coco")}, } diff --git a/wiki/adding-a-platform.md b/wiki/adding-a-platform.md index ac987bcfd..3ca46cac4 100644 --- a/wiki/adding-a-platform.md +++ b/wiki/adding-a-platform.md @@ -429,6 +429,7 @@ launchers: | `repo` | What the package fetches and builds, as the script writes it (`rp_module_repo`: method, URL, branch or commit) | | `systems` | Systems the package registers, under the platform's names (`addSystem`, `addPort` for ports) | | `slot` | The key the code files the list under: a region, a machine | +| `machines` | On a record naming systems, the machines the platform offers the emulator under them (RetroBat's `xroar_machine` choices for coco) | | `names` | In the order the code tries or writes them, relative to `search_dir` | | `expands` | Values a `{X}` placeholder in `names` takes, in the order tried (Hatari tries each TOS in ten languages) | | `case_insensitive` | Whether the launcher matches names regardless of case |