From 8ec0c5482cef017a85589f8badf770d451b73541 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Mon, 9 Nov 2026 19:41:40 +0100 Subject: [PATCH] fix: place xroar ROMs in each rompath XRoar reads a ROM by name from the directories of rompath. The entries take their file name as path, relative to ~/.xroar/roms or ${prefix}/share/xroar/roms, and rompath and -rompath are declared as search lists. Batocera records its rompath line, Recalbox -rompath bios/ (the placeholder its extractor already writes), RetroBat its -rompath as two records, bios/dragon and bios/coco for coco. An entry whose name the platform list does not declare follows the launch its profile system is listed under: Recalbox lists mc10.rom under trs80coco, so alice.rom is read there. Batocera ROMs move from the BIOS root to bios/xroar, Recalbox ROMs to bios/dragon and bios/trs80coco, RetroBat ROMs to bios/dragon and bios/coco. RetroDECK and RetroPie keep them at the BIOS root. --- emulators/xroar.yml | 63 +++++++++++++++++++++++++-- platforms/batocera.yml | 8 ++++ platforms/recalbox.yml | 8 ++++ platforms/retrobat.yml | 14 +++++- schemas/platform.schema.json | 2 +- scripts/common.py | 15 ++++++- scripts/launchers.py | 7 +-- scripts/scraper/batocera_launchers.py | 4 +- scripts/scraper/recalbox_launchers.py | 29 +++++++++++- scripts/scraper/retrobat_launchers.py | 37 +++++++++++++++- tests/test_launchers_recalbox.py | 39 ++++++++++++++++- tests/test_launchers_retrobat.py | 13 +++++- tests/test_standalone_links.py | 59 ++++++++++++++++++++++++- 13 files changed, 279 insertions(+), 19 deletions(-) diff --git a/emulators/xroar.yml b/emulators/xroar.yml index 2fd685b91..3aa57fb79 100644 --- a/emulators/xroar.yml +++ b/emulators/xroar.yml @@ -1,5 +1,6 @@ emulator: XRoar type: standalone +core_classification: other source: "https://www.6809.org.uk/git/xroar.git" upstream: "https://www.6809.org.uk/xroar/" version_source: @@ -21,13 +22,23 @@ systems: - coco - coco3 - mc10 +standalone_directories: + - "~/.xroar/roms" + - "${prefix}/share/xroar/roms" +standalone_pools: + rompath: "" + "-rompath": "" notes: | XRoar by Ciaran Anscomb emulates Dragon 32/64, Tandy CoCo 1/2/3, MC-10, Alice, and clones (MX-1600, CP-400, Dragon 200-E, Deluxe CoCo). - ROM files are searched in a configurable rompath directory with automatic - extension appending (.rom, .ROM, .dgn, .DGN). Batocera sets rompath to - /userdata/bios/xroar/. + A ROM is looked up by name in the directories of rompath, a colon-separated + list searched in order: the name as given first, then with .rom, .ROM, .dgn + and .DGN appended, and the first file found is read (src/romlist.c:47-49, + 124-143, src/path.c:158-219). rompath is set in xroar.conf or with + -rompath (src/xroar.c:3266). A Unix build defaults to + ~/.xroar/roms:$(datadir)/xroar/roms, $(datadir) being ${prefix}/share + unless configure is told otherwise (src/Makefile.am:236). The romlist mechanism defines fallback filenames per ROM slot. Each machine type has a default romlist that tries filenames in priority order. CRC32 is @@ -44,6 +55,7 @@ files: # ---- Dragon 32 ---- - name: "d32.rom" + path: "d32.rom" system: dragon32 description: "Dragon 32 BASIC ROM" required: true @@ -57,6 +69,7 @@ files: # ---- Dragon 64 ---- - name: "d64_1.rom" + path: "d64_1.rom" system: dragon64 description: "Dragon 64 32K mode BASIC ROM" required: true @@ -68,6 +81,7 @@ files: note: "IC17 ROM. Required for the machine to start. Romlist d64_1 tries d64_1, d64rom1, Dragon Data Ltd - Dragon 64 - IC17, Dragon Data Ltd - TANO IC18, Eurohard S.A. - Dragon 200 IC18, dragrom." - name: "d64_2.rom" + path: "d64_2.rom" system: dragon64 description: "Dragon 64 64K mode BASIC ROM" required: false @@ -81,6 +95,7 @@ files: # ---- Dragon 200-E ---- - name: "d200e_1.rom" + path: "d200e_1.rom" system: dragon64 description: "Dragon 200-E BASIC IC18 (Spanish)" required: true @@ -92,6 +107,7 @@ files: note: "Required for Dragon 200-E. Falls back to d64_1 then generic dragon if not found. Romlist d200e_1 tries d200e_1, d200e_rom1, ic18_v1.4e.ic34." - name: "d200e_2.rom" + path: "d200e_2.rom" system: dragon64 description: "Dragon 200-E BASIC IC17 (Spanish)" required: false @@ -102,6 +118,7 @@ files: note: "64K mode alternate. Falls back to d64_2 if not found. Romlist d200e_2 tries d200e_2, d200e_rom2, ic17_v1.4e.ic37." - name: "d200e_26.rom" + path: "d200e_26.rom" system: dragon64 description: "Dragon 200-E external character generator ROM" required: false @@ -114,6 +131,7 @@ files: # ---- Dragon Professional (Alpha) ---- - name: "alpha-boot-v1.0.rom" + path: "alpha-boot-v1.0.rom" system: dragon64 description: "Dragon Professional (Alpha) boot ROM v1.0" required: true @@ -129,6 +147,7 @@ files: Romlist dragonpro_boot tries alpha-boot-v1.0, alpha_bt_10, Dragon Data Ltd - Dragon Alpha Boot v1.0 150584, alpha-boot-v0.4, alpha_bt_04. - name: "alpha-boot-v0.4.rom" + path: "alpha-boot-v0.4.rom" system: dragon64 description: "Dragon Professional (Alpha) boot ROM v0.4" required: false @@ -145,6 +164,7 @@ files: Romlist dragonpro_boot tries alpha-boot-v1.0, alpha_bt_10, Dragon Data Ltd - Dragon Alpha Boot v1.0 150584, alpha-boot-v0.4, alpha_bt_04. - name: "alpha-basic.rom" + path: "alpha-basic.rom" system: dragon64 description: "Dragon Professional (Alpha) BASIC ROM" required: true @@ -158,6 +178,7 @@ files: # ---- Tandy CoCo (Colour BASIC) ---- - name: "bas13.rom" + path: "bas13.rom" system: coco description: "Tandy CoCo Colour BASIC 1.3" required: true @@ -168,6 +189,7 @@ files: note: "Default Colour BASIC. First tried in the @coco romlist. 8K ROM loaded into slot 1 of a 2-slot rombank." - name: "bas12.rom" + path: "bas12.rom" system: coco description: "Tandy CoCo Colour BASIC 1.2" required: false @@ -179,6 +201,7 @@ files: note: "Alternative to bas13. Second in fallback order. Romlist coco tries bas13, bas12, Color Basic v1.2 (1982)(Tandy), bas11, bas10. Romlist coco2 tries bas12, @coco." - name: "bas11.rom" + path: "bas11.rom" system: coco description: "Tandy CoCo Colour BASIC 1.1" required: false @@ -189,6 +212,7 @@ files: note: "Used by CoCo 1E. Third in fallback order." - name: "bas10.rom" + path: "bas10.rom" system: coco description: "Tandy CoCo Colour BASIC 1.0" required: false @@ -201,6 +225,7 @@ files: # ---- Tandy CoCo (Extended Colour BASIC) ---- - name: "extbas11.rom" + path: "extbas11.rom" system: coco description: "Tandy CoCo Extended Colour BASIC 1.1" required: true @@ -211,6 +236,7 @@ files: note: "Default Extended BASIC. 8K ROM loaded into slot 0 of a 2-slot rombank." - name: "extbas10.rom" + path: "extbas10.rom" system: coco description: "Tandy CoCo Extended Colour BASIC 1.0" required: false @@ -223,6 +249,7 @@ files: # ---- Tandy CoCo 3 ---- - name: "coco3.rom" + path: "coco3.rom" system: coco3 description: "CoCo 3 Super Extended Colour BASIC (NTSC)" required: true @@ -233,6 +260,7 @@ files: note: "32K combined ROM. Single required ROM for CoCo 3 NTSC." - name: "coco3p.rom" + path: "coco3p.rom" system: coco3 description: "CoCo 3 Super Extended Colour BASIC (PAL)" required: true @@ -245,6 +273,7 @@ files: # ---- Tandy MC-10 ---- - name: "mc10.rom" + path: "mc10.rom" system: mc10 description: "Tandy MC-10 Microcolour BASIC" required: true @@ -255,6 +284,7 @@ files: note: "Single required ROM for MC-10." - name: "alice.rom" + path: "alice.rom" system: mc10 description: "Matra & Hachette Alice Microcolour BASIC" required: true @@ -267,6 +297,7 @@ files: # ---- Deluxe CoCo (prototype) ---- - name: "deluxe.rom" + path: "deluxe.rom" system: coco description: "Tandy Deluxe CoCo Advanced BASIC (bank 0)" required: true @@ -277,18 +308,21 @@ files: note: "Primary ROM for the Deluxe CoCo prototype. Loaded as extbas_rom. Romlist deluxecoco tries adv070_u24, deluxe." - name: "adv071_u24.rom" + path: "adv071_u24.rom" system: coco description: "Tandy Deluxe CoCo ROM bank 1" required: false source_ref: "src/xroar.c:681 (romlist deluxecoco1), src/dragon/deluxecoco.c:223-226 (load)" - name: "adv072_u24.rom" + path: "adv072_u24.rom" system: coco description: "Tandy Deluxe CoCo ROM bank 2" required: false source_ref: "src/xroar.c:682 (romlist deluxecoco2), src/dragon/deluxecoco.c:230-233 (load)" - name: "adv073-2_u24.rom" + path: "adv073-2_u24.rom" system: coco description: "Tandy Deluxe CoCo ROM bank 3" required: false @@ -299,6 +333,7 @@ files: # ---- Dynacom MX-1600 (Brazilian clone) ---- - name: "mx1600bas.rom" + path: "mx1600bas.rom" system: coco description: "Dynacom MX-1600 Colour BASIC" required: true @@ -309,6 +344,7 @@ files: note: "Required for MX-1600. Zephyr-patched variant (CRC32 d11b1c96) is an alias. Romlist mx1600 tries mx1600bas, mx1600bas_zephyr." - name: "mx1600extbas.rom" + path: "mx1600extbas.rom" system: coco description: "Dynacom MX-1600 Extended Colour BASIC" required: true @@ -319,6 +355,7 @@ files: # ---- Prologica CP-400 (Brazilian clone) ---- - name: "cp400bas.rom" + path: "cp400bas.rom" system: coco description: "Prologica CP-400 Colour BASIC" required: true @@ -327,6 +364,7 @@ files: source_ref: "src/xroar.c:688 (romlist cp400), src/xroar.c:755 (crclist cp400)" - name: "cp400ext.rom" + path: "cp400ext.rom" system: coco description: "Prologica CP-400 Extended Colour BASIC" required: true @@ -337,6 +375,7 @@ files: # ---- DOS cartridge ROMs (all optional) ---- - name: "disk11.rom" + path: "disk11.rom" system: coco description: "Tandy RS-DOS 1.1 (CoCo floppy disk controller)" required: false @@ -346,6 +385,7 @@ files: note: "Primary CoCo disk controller ROM. disk10.rom (RS-DOS 1.0) is an earlier version used as fallback." - name: "disk10.rom" + path: "disk10.rom" system: coco description: "Tandy RS-DOS 1.0 (CoCo floppy disk controller)" required: false @@ -355,6 +395,7 @@ files: note: "Second name in the rsdos romlist, opened when disk11.rom is absent." - name: "hdbdw3bck.rom" + path: "hdbdw3bck.rom" system: coco description: "RS-DOS with Becker port (DriveWire)" required: false @@ -362,6 +403,7 @@ files: note: "For CoCo with Becker port hardware. Enables DriveWire connectivity." - name: "ddos12a.rom" + path: "ddos12a.rom" system: dragon64 description: "DragonDOS 1.2a (Dragon floppy disk controller)" required: false @@ -369,6 +411,7 @@ files: note: "Latest DragonDOS version. Fallback chain tries DOSPlus -> SuperDOS -> DragonDOS -> Cumana. Older versions: ddos12, ddos40, ddos15, ddos10." - name: "ddos40.rom" + path: "ddos40.rom" system: dragon64 description: "DragonDOS 4.0 (Eurohard S.A.)" required: false @@ -378,6 +421,7 @@ files: note: "Third name in the dragondos romlist, which @dragondos_compat tries after the dosplus and superdos lists." - name: "ddos10.rom" + path: "ddos10.rom" system: dragon64 description: "DragonDOS 1.0 (Dragon Data Ltd)" required: false @@ -388,6 +432,7 @@ files: note: "Fifth name in the dragondos romlist, which @dragondos_compat tries after the dosplus and superdos lists. Romlist dragondos tries ddos12a, ddos12, ddos40, ddos15, ddos10, Dragon Data Ltd - DragonDOS 1.0." - name: "dplus49b.rom" + path: "dplus49b.rom" system: dragon64 description: "DOSPlus 4.9b (Dragon compatible DOS)" required: false @@ -397,6 +442,7 @@ files: note: "First tried in DragonDOS-compatible cartridge. Enhanced Dragon DOS. Older version: dplus48 (alias: dosplus-4.8, DOSPLUS)." - name: "dplus48.rom" + path: "dplus48.rom" system: dragon64 description: "DOSPlus 4.8 (Dragon compatible DOS)" required: false @@ -407,6 +453,7 @@ files: note: "Second name in the dosplus romlist, the first list @dragondos_compat tries. Romlist dosplus tries dplus49b, dplus48, dosplus-4.8, DOSPLUS." - name: "sdose6.rom" + path: "sdose6.rom" system: dragon64 description: "SuperDOS E6 (Dragon compatible DOS)" required: false @@ -417,6 +464,7 @@ files: note: "Latest SuperDOS. Older versions: sdose5, sdose4. Romlist superdos tries sdose6, PNP - SuperDOS E6, sdose5, sdose4." - name: "cdos20.rom" + path: "cdos20.rom" system: dragon64 description: "Cumana DOS 2.0 (Dragon compatible DOS)" required: false @@ -425,6 +473,7 @@ files: note: "Romlist cumana tries cdos20, CDOS20, CumanaDOSv2.0." - name: "delta2.rom" + path: "delta2.rom" system: dragon64 description: "Delta System 2 (Premier Microsystems disk controller)" required: false @@ -435,6 +484,7 @@ files: note: "Latest Delta version. Older versions: delta1a, delta19, delta, deltados. Romlist delta tries delta2, delta1a, delta19, delta, deltados, Premier Micros - DeltaDOS." - name: "cp450dsk.rom" + path: "cp450dsk.rom" system: coco description: "Prologica CP-450 DOS cartridge" required: false @@ -444,6 +494,7 @@ files: # ---- Other cartridge ROMs (all optional) ---- - name: "orch90.rom" + path: "orch90.rom" system: coco description: "Orchestra-90 CC sound cartridge ROM" required: false @@ -453,6 +504,7 @@ files: note: "Sound synthesis cartridge. Loaded directly (no romlist fallback)." - name: "mcx128.rom" + path: "mcx128.rom" system: mc10 description: "MCX128 memory expansion BASIC ROM" required: false @@ -461,6 +513,7 @@ files: note: "128K memory expansion for MC-10. Adds enhanced BASIC. Romlist mcx128 tries mcx128, MCX_Basic_21." - name: "mcx128a.rom" + path: "mcx128a.rom" system: mc10 description: "MCX128 memory expansion BASIC ROM (AZERTY)" required: false @@ -469,6 +522,7 @@ files: note: "AZERTY keyboard variant for Alice. Same MCX128 hardware. Romlist mcx128_azerty tries mcx128a, MCX_Basic_21_AZERTY." - name: "yados.rom" + path: "yados.rom" system: coco description: "YADOS (Glenside IDE interface ROM)" required: false @@ -476,6 +530,7 @@ files: note: "IDE hard disk interface. Fallback: hdblba.rom (alternative IDE ROM)." - name: "sdbdos.rom" + path: "sdbdos.rom" system: dragon64 description: "SDBDOS (MOOH memory cartridge ROM)" required: false @@ -483,6 +538,7 @@ files: note: "SD card boot DOS for MOOH cartridge. Loaded directly." - name: "dragonfly-2.3.rom" + path: "dragonfly-2.3.rom" system: dragon64 description: "Ikon Ultra Drive Dragonfly ROM 2.3" required: false @@ -490,6 +546,7 @@ files: note: "Experimental, compiled only with WANT_EXPERIMENTAL. Ikon Ultra Drive storage interface. First candidate of the ikon romlist. No crclist entry names it." - name: "dragonfly-1.3.rom" + path: "dragonfly-1.3.rom" system: dragon64 description: "Ikon Ultra Drive Dragonfly ROM 1.3" required: false diff --git a/platforms/batocera.yml b/platforms/batocera.yml index 95c94a27c..291b8dc7d 100644 --- a/platforms/batocera.yml +++ b/platforms/batocera.yml @@ -6580,6 +6580,14 @@ launchers: \ -position 2" fails_when_missing: false source_ref: "package/batocera/core/batocera-configgen/configgen/configgen/generators/openmsx/openmsxGenerator.py:89" +- emulator: xroar + build: standalone + kind: passes_path + names: [] + search_dir: /userdata/bios/xroar + passes_to: "written: rompath /userdata/bios/xroar" + fails_when_missing: false + source_ref: "package/batocera/core/batocera-configgen/configgen/configgen/generators/xroar/xroarGenerator.py:31-32" - emulator: o2em build: libretro kind: core_option diff --git a/platforms/recalbox.yml b/platforms/recalbox.yml index 13249dcda..3d7ad8e97 100644 --- a/platforms/recalbox.yml +++ b/platforms/recalbox.yml @@ -4171,6 +4171,14 @@ launchers: fails_when_missing: false depends_on_option: system 64dd source_ref: "projects/configgen/configgen/generators/mupen/mupenConfig.py:118-121" +- emulator: xroar + build: standalone + kind: passes_path + names: [] + search_dir: /recalbox/share/bios/ + passes_to: command line -rompath + fails_when_missing: false + source_ref: "projects/configgen/configgen/generators/xroar/xroarGenerator.py:25" - emulator: dolphin, dolphin-gui build: standalone kind: fixed_name diff --git a/platforms/retrobat.yml b/platforms/retrobat.yml index 540f55dea..4f38a28d7 100644 --- a/platforms/retrobat.yml +++ b/platforms/retrobat.yml @@ -5373,10 +5373,20 @@ launchers: build: standalone kind: passes_path names: [] - search_dir: bios/dragon, or bios/coco when system == "coco" - passes_to: command line -rompath bios/dragon, or bios/coco when system == "coco" + search_dir: bios/dragon + passes_to: command line -rompath bios/dragon fails_when_missing: false source_ref: "emulatorLauncher/Generators/Xroar.Generator.cs:76-80" +- emulator: xroar + build: standalone + kind: passes_path + names: [] + search_dir: bios/coco + passes_to: command line -rompath bios/coco + fails_when_missing: false + systems: + - coco + source_ref: "emulatorLauncher/Generators/Xroar.Generator.cs:76-80" - emulator: xroar build: standalone kind: passes_path diff --git a/schemas/platform.schema.json b/schemas/platform.schema.json index 16e061f17..9e63e0dba 100644 --- a/schemas/platform.schema.json +++ b/schemas/platform.schema.json @@ -270,7 +270,7 @@ }, "search_dir": { "type": "string", - "description": "Directory the launcher looks in, as the code resolves it. {system} stands for the platform system the emulator is launched for" + "description": "Directory the launcher looks in, as the code resolves it. stands for the platform system the emulator is launched for" }, "passes_to": { "type": "string", diff --git a/scripts/common.py b/scripts/common.py index 05a3ffc73..00f80ac90 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -2945,9 +2945,22 @@ def platform_layouts( profile.get("standalone_directories") or profile.get("standalone_pools") ) and any(h.systems and h.emulator in names for h in _handed(standalone_cores)) index = system_file_index(standalone_cores) if per_system else {} + # A profile system the platform launches under a system of its own, as + # its list says for one entry, is launched there for every entry: + # Recalbox lists mc10.rom under trs80coco, so alice.rom is read there. + family: dict[str, set[str]] = {} + for f in (profile.get("files") or []) if per_system else []: + if isinstance(f, dict): + own = entry_launch_systems(f, {}) + for system in own: + family.setdefault(system, set()).update(entry_launch_systems(f, index) - own) def bindings_of(entry: dict) -> dict[str, tuple[str, str]]: - systems = entry_launch_systems(entry, index) if per_system else frozenset() + systems = frozenset() + if per_system: + 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 not in by_systems: by_systems[systems] = standalone_bindings_for( emu_name, profile, standalone_cores, systems diff --git a/scripts/launchers.py b/scripts/launchers.py index c19de9aea..1f2119d8d 100644 --- a/scripts/launchers.py +++ b/scripts/launchers.py @@ -399,8 +399,9 @@ class HandedDirectory: # The placeholder a launcher record writes for the system it is launched -# for: Recalbox passes -rompath os.path.join(BIOS, args.system) to XRoar. -SYSTEM_PLACEHOLDER = "{system}" +# for, as Recalbox's extractor spells it: Recalbox passes XRoar -rompath +# os.path.join(BIOS, args.system). +SYSTEM_PLACEHOLDER = "" def native_systems(config: dict) -> dict[str, str]: @@ -442,7 +443,7 @@ def handed_directories(config: dict) -> list[HandedDirectory]: describing several directories in prose names no single one. A search path (XRoar's rompath) gives each of its directories, in order. A record naming systems applies to those systems only, and one whose search_dir - holds {system} gives one directory per system the platform declares. + holds gives one directory per system the platform declares. """ base = str(config.get("base_destination") or "bios") natives = native_systems(config) diff --git a/scripts/scraper/batocera_launchers.py b/scripts/scraper/batocera_launchers.py index 8f62babcf..c1a6ae13a 100644 --- a/scripts/scraper/batocera_launchers.py +++ b/scripts/scraper/batocera_launchers.py @@ -901,8 +901,8 @@ def environment_directories(cg: Configgen) -> list[Launcher]: # Generators that hand the emulator a /userdata/bios directory through its # configuration: a line written to the file it reads (openMSX's script.tcl -# filepool entries) or a key saved to it. -CONFIG_DIRECTORY_GENERATORS = ("openmsx.openmsxGenerator",) +# filepool entries, xroar.conf rompath) or a key saved to it. +CONFIG_DIRECTORY_GENERATORS = ("openmsx.openmsxGenerator", "xroar.xroarGenerator") def config_directories( diff --git a/scripts/scraper/recalbox_launchers.py b/scripts/scraper/recalbox_launchers.py index 36f148358..fc0ce3297 100644 --- a/scripts/scraper/recalbox_launchers.py +++ b/scripts/scraper/recalbox_launchers.py @@ -39,6 +39,7 @@ CORES = f"{GENERATORS}/libretro/libretroCores.py" LIBRETRO_GENERATOR = f"{GENERATORS}/libretro/libretroGenerator.py" HATARI = f"{GENERATORS}/hatari/hatariGenerator.py" GSPLUS = f"{GENERATORS}/gsplus/gsplusGenerator.py" +XROAR = f"{GENERATORS}/xroar/xroarGenerator.py" AMIBERRY_GENERATOR = f"{GENERATORS}/amiberry/amiberryGenerator.py" AMIBERRY_KICKSTARTS = f"{GENERATORS}/amiberry/amiberryKickstarts.py" AMIBERRY_SUBSYSTEMS = f"{GENERATORS}/amiberry/amiberrySubSystems.py" @@ -52,7 +53,7 @@ MUPEN_CONFIG = f"{GENERATORS}/mupen/mupenConfig.py" SOURCE_FILES = ( FILES_MODULE, LAUNCHER, RETROARCH, CORES, LIBRETRO_GENERATOR, HATARI, GSPLUS, AMIBERRY_KICKSTARTS, AMIBERRY_SUBSYSTEMS, AMIBERRY_CONFIG, PCSX2, DOLPHIN, XEMU, - MUPEN_CONFIG, + MUPEN_CONFIG, XROAR, ) # Recalbox's system.ini runs every libretro core as `emulator = libretro`: @@ -1171,8 +1172,32 @@ def core_file_options(cg: Configgen, directories: list[Launcher]) -> list[Launch return launchers +def xroar(cg: Configgen) -> list[Launcher]: + """-rompath os.path.join(recalboxFiles.BIOS, args.system) on the command line.""" + src = cg.source(XROAR) + generate = src.function("generate") + # args.system is the ES system name, as self.system.Name is elsewhere. + env = {**cg.env(src, generate), "args.system": SYSTEM_PLACEHOLDER} + found = [ + (flag, value) + for node in ast.walk(generate) + if isinstance(node, ast.List) + for flag, value in zip(node.elts, node.elts[1:]) + if isinstance(flag, ast.Constant) and flag.value == "-rompath" + ] + if len(found) != 1: + src.fail(f"expected one -rompath on the command line, found {len(found)}") + flag, value = found[0] + directory = cg.bios_dir(spell(value, env), src, "-rompath") + return [Launcher( + emulator=cg.emulator(XROAR), build="standalone", kind="passes_path", names=[], + search_dir=directory, passes_to="command line -rompath", + source_ref=src.ref([(flag.lineno, value.end_lineno)]), + )] + + STANDALONE: tuple[Callable[[Configgen], list[Launcher]], ...] = ( - hatari, gsplus, amiberry, pcsx2, xemu, mupen64plus, + hatari, gsplus, amiberry, pcsx2, xemu, mupen64plus, xroar, ) LIBRETRO: tuple[Callable[[Configgen], list[Launcher]], ...] = ( core_assets, hatari_libretro, content_files, diff --git a/scripts/scraper/retrobat_launchers.py b/scripts/scraper/retrobat_launchers.py index b15ea4f3d..b1c960b51 100644 --- a/scripts/scraper/retrobat_launchers.py +++ b/scripts/scraper/retrobat_launchers.py @@ -2976,7 +2976,11 @@ def xroar(sources: dict[str, Source]) -> list[Record]: found = _argument_paths(src, "xroar", "standalone") if not found: raise LauncherPatternMissing(src.path, "rompath argument") - records = [_with_conditions(record, conditions) for record, conditions in found] + records = [ + split + for record, conditions in found + for split in _per_system(src, _with_conditions(record, conditions)) + ] line = src.find(r'string (\w+) = ("rompath (?:[^"\\\n]|\\.)*");', "config rompath line") written = _unescape(line.group(2)[1:-1]) relative = re.fullmatch(r'rompath "(.*)"', written) @@ -3003,6 +3007,37 @@ def xroar(sources: dict[str, Source]) -> list[Record]: return _covered(src, records) +_SYSTEM_BRANCH = re.compile(r'(\S+), or (\S+) when system == "([\w.-]+)"') + + +def _per_system(src: Source, record: Record) -> list[Record]: + """A directory chosen by the system, one record per choice. + + `romPath = ; if (system == "coco") romPath = ;` renders as ", or + when system == "coco"": is passed for that system, for every + other one. + """ + rendered = str(record.get("search_dir", "")) + branch = _SYSTEM_BRANCH.fullmatch(rendered) + if branch is None: + if " or " in rendered: + raise LauncherPatternMissing(src.path, f"directory choice {rendered!r}") + return [record] + default, chosen, system = branch.groups() + flag = str(record["passes_to"]).removesuffix(rendered).strip() + out = [] + for directory, systems in ((default, None), (chosen, [system])): + split = {k: v for k, v in record.items() if k != "source_ref"} + split.update( + names=list(record["names"]), search_dir=directory, passes_to=f"{flag} {directory}" + ) + if systems: + split["systems"] = systems + split["source_ref"] = record["source_ref"] + out.append(split) + return out + + def pico8(sources: dict[str, Source]) -> list[Record]: """The PICO-8 binary the generator runs, from an emulator folder. diff --git a/tests/test_launchers_recalbox.py b/tests/test_launchers_recalbox.py index 381920bc9..9d53046fd 100644 --- a/tests/test_launchers_recalbox.py +++ b/tests/test_launchers_recalbox.py @@ -41,6 +41,7 @@ from scraper.recalbox_launchers import ( # noqa: E402 RETROARCH, SOURCE_FILES, XEMU, + XROAR, Configgen, Launcher, LauncherShapeError, @@ -57,6 +58,7 @@ from scraper.recalbox_launchers import ( # noqa: E402 pcsx2, system_directories, xemu, + xroar, ) from tests.test_launchers import placed # noqa: E402 @@ -109,6 +111,9 @@ LAUNCHER_BLOCKS = { 80: ''' elif emulator == "pcsx2": module = __import__("configgen.generators.pcsx2.pcsx2Generator", fromlist=["Pcsx2Generator"]) generatorClass = getattr(module, "Pcsx2Generator")''', + 122: ''' elif emulator == "xroar": + module = __import__("configgen.generators.xroar.xroarGenerator", fromlist=["XroarGenerator"]) + generatorClass = getattr(module, "XroarGenerator")''', 125: ''' elif emulator == "xemu": module = __import__("configgen.generators.xemu.xemuGenerator", fromlist=["XemuGenerator"]) generatorClass = getattr(module, "XemuGenerator")''', @@ -1184,6 +1189,22 @@ from configgen.controllers.controller import InputItem, ControllerPerPlayer, Con mupenSettings.setString(section, "IPL-ROM", "")''', } +# xroarGenerator.py:1-26 at 10.1.1 +XROAR_BLOCKS = { + 8: '''class XroarGenerator(Generator): + + def generate(self, system: Emulator, playersControllers: ControllerPerPlayer, recalboxOptions: keyValueSettings, args) -> Command: + + # Make save dir + import os + import configgen.recalboxFiles as recalboxFiles''', + 22: ''' snapFile: str = os.path.basename(args.rom) + ".sna" + commandArray: List[str] = [recalboxFiles.recalboxBins[system.Emulator], + "-vo", "sdl", "-ui", "sdl", "-ao", "pulse", + "-rompath", os.path.join(recalboxFiles.BIOS, args.system), + "-snappath", os.path.join(snapshotFolder, snapFile)]''', +} + BLOCKS = { FILES_MODULE: FILES_MODULE_BLOCKS, LAUNCHER: LAUNCHER_BLOCKS, @@ -1199,6 +1220,7 @@ BLOCKS = { DOLPHIN: DOLPHIN_BLOCKS, XEMU: XEMU_BLOCKS, MUPEN_CONFIG: MUPEN_CONFIG_BLOCKS, + XROAR: XROAR_BLOCKS, } @@ -1220,6 +1242,21 @@ def ref(path: str, lines: str) -> str: return f"{path}:{lines}" +class XroarRomPath(unittest.TestCase): + def test_the_rom_path_is_the_bios_folder_of_the_system(self): + (record,) = xroar(configgen()) + self.assertEqual( + (record.emulator, record.kind, record.search_dir, record.passes_to, record.source_ref), + ("xroar", "passes_path", "/recalbox/share/bios/", "command line -rompath", + ref(XROAR, "25")), + ) + + def test_a_rom_path_built_another_way_raises(self): + cg = configgen(XROAR, "os.path.join(recalboxFiles.BIOS, args.system)", "args.rompath") + with self.assertRaises(LauncherShapeError): + xroar(cg) + + class SystemDirectoryPerCore(unittest.TestCase): """libretroRetroarch.py:431-529, the directory RetroArch is given.""" @@ -1511,7 +1548,7 @@ class ExtractionNeverEmpty(unittest.TestCase): records = extract_launchers(reader) self.assertEqual(sorted(set(read)), sorted(SOURCE_FILES)) - self.assertEqual(len(records), 85) + self.assertEqual(len(records), 86) def test_records_fit_the_schema(self): schema = json.loads((REPO_ROOT / "schemas" / "platform.schema.json").read_text()) diff --git a/tests/test_launchers_retrobat.py b/tests/test_launchers_retrobat.py index 5cd4a0ec9..da7832502 100644 --- a/tests/test_launchers_retrobat.py +++ b/tests/test_launchers_retrobat.py @@ -1149,8 +1149,17 @@ class FolderArguments(unittest.TestCase): ) def test_xroar_reassigned_folder_and_config_line(self): - argument, config = rl.xroar(_sources(**{f"{G}/Xroar.Generator.cs": XROAR})) - self.assertEqual(argument["search_dir"], 'bios/dragon, or bios/coco when system == "coco"') + # romPath is bios/dragon, rewritten to bios/coco for the coco system. + default, coco, config = rl.xroar(_sources(**{f"{G}/Xroar.Generator.cs": XROAR})) + self.assertEqual( + (default["search_dir"], default["passes_to"], default.get("systems")), + ("bios/dragon", "command line -rompath bios/dragon", None), + ) + self.assertEqual( + (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(config["search_dir"], "bios/dragon") self.assertEqual( config["passes_to"], 'xroar.conf rompath ".\\\\..\\\\..\\\\bios\\\\dragon"' diff --git a/tests/test_standalone_links.py b/tests/test_standalone_links.py index e0e4a9728..1aaf62c6c 100644 --- a/tests/test_standalone_links.py +++ b/tests/test_standalone_links.py @@ -259,7 +259,7 @@ class PerSystem(unittest.TestCase): def test_a_system_placeholder_gives_one_directory_per_system(self) -> None: # Recalbox passes -rompath os.path.join(BIOS, args.system). cfg = { - **config(passes("emu", "/recalbox/share/bios/{system}", "command line -rompath")), + **config(passes("emu", "/recalbox/share/bios/", "command line -rompath")), "systems": { "coco": {"native_id": "trs80coco", "files": []}, "dragon64": {"native_id": "dragon", "files": []}, @@ -285,6 +285,29 @@ class PerSystem(unittest.TestCase): ) +class SystemFamily(unittest.TestCase): + def test_an_entry_follows_the_launch_its_system_is_listed_under(self) -> None: + # Recalbox lists mc10.rom under trs80coco: alice.rom, of the same + # profile system, is read there too. + profile = { + "emulator": "Emu", "type": "standalone", "standalone_pools": {"-rompath": ""}, + "files": [ + {"name": "mc10.rom", "path": "mc10.rom", "system": "mc10"}, + {"name": "alice.rom", "path": "alice.rom", "system": "mc10"}, + ], + } + cfg = { + **config(passes("emu", "/recalbox/share/bios/", "-rompath")), + "systems": {"coco": {"native_id": "trs80coco", + "files": [{"name": "mc10.rom"}]}}, + } + ((view, _standalone),) = platform_layouts("emu", profile, standalone_set(cfg)) + self.assertEqual( + [f.get("standalone_path") for f in view["files"]], + ["trs80coco/mc10.rom", "trs80coco/alice.rom"], + ) + + class Layout(unittest.TestCase): LINKED = link("emu", "$biosdir/dc", "$home/.local/share/emu is a symlink to it") @@ -537,3 +560,37 @@ class OpenmsxOnEachPlatform(unittest.TestCase): self.assertTrue( entry["standalone_path"].startswith("openmsx/"), entry["standalone_path"] ) + + +class XroarOnEachPlatform(unittest.TestCase): + """XRoar reads each ROM from a directory of rompath. Batocera writes + rompath bios/xroar, RetroDECK lists $bios_path in it, RetroPie links + the build's ${prefix}/share/xroar/roms to $biosdir, RetroBat passes + -rompath bios/coco for coco and bios/dragon otherwise, Recalbox + -rompath bios/.""" + + # Platform -> profile system -> folder, "" for the BIOS root. + EXPECTED = { + "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"}, + "recalbox": {**dict.fromkeys(("dragon32", "dragon64"), "dragon"), + **dict.fromkeys(("coco", "coco3", "mc10"), "trs80coco")}, + } + + @classmethod + def setUpClass(cls) -> None: + cls.profile = load_emulator_profiles(str(REPO_ROOT / "emulators"))["xroar"] + + def test_each_rom_lands_where_its_launch_reads_it(self) -> None: + for platform, folders in self.EXPECTED.items(): + cfg = load_platform_config(platform, str(REPO_ROOT / "platforms")) + (view, _standalone), = platform_layouts("xroar", self.profile, standalone_set(cfg)) + for entry in view["files"]: + with self.subTest(platform=platform, name=entry["name"]): + self.assertNotIn("unplaced", entry) + folder = folders[entry["system"]] + expected = f"{folder}/{entry['name']}" if folder else entry["name"] + self.assertEqual(entry["standalone_path"], expected)