diff --git a/platforms/retrodeck.yml b/platforms/retrodeck.yml index 5e9da15d..3506c588 100644 --- a/platforms/retrodeck.yml +++ b/platforms/retrodeck.yml @@ -16040,3 +16040,952 @@ systems: native_system: coco component: xroar description: Generic disk ROM +launchers: +- emulator: cemu + build: standalone + kind: links + names: + - keys.txt + case_insensitive: false + search_dir: $bios_path/cemu + passes_to: $XDG_DATA_HOME/Cemu/keys.txt is a symlink to it + fails_when_missing: false + depends_on_option: "[[ -e \"$bios_path/cemu/keys.txt\" ]]" + source_ref: "cemu/component_prepare.sh:17" +- emulator: cemu + build: standalone + kind: passes_path + names: [] + search_dir: $bios_path/cemu + passes_to: settings.xml mlc_path + fails_when_missing: false + source_ref: "cemu/component_prepare.sh:14, 29" +- emulator: dolphin + build: standalone + kind: links + slot: EUR + names: [] + search_dir: $saves_path/gc/dolphin/EU + passes_to: $XDG_DATA_HOME/dolphin-emu/GC/EUR is a symlink to it + fails_when_missing: false + source_ref: "dolphin/component_prepare.sh:19, 38" +- emulator: dolphin + build: standalone + kind: links + slot: USA + names: [] + search_dir: $saves_path/gc/dolphin/US + passes_to: $XDG_DATA_HOME/dolphin-emu/GC/USA is a symlink to it + fails_when_missing: false + source_ref: "dolphin/component_prepare.sh:20, 39" +- emulator: dolphin + build: standalone + kind: links + slot: JAP + names: [] + search_dir: $saves_path/gc/dolphin/JP + passes_to: $XDG_DATA_HOME/dolphin-emu/GC/JAP is a symlink to it + fails_when_missing: false + source_ref: "dolphin/component_prepare.sh:21, 40" +- emulator: dolphin + build: standalone + kind: links + names: [] + search_dir: $bios_path/Triforce + passes_to: $XDG_DATA_HOME/dolphin-emu/Triforce is a symlink to it + fails_when_missing: false + source_ref: "dolphin/component_prepare.sh:30, 49" +- emulator: dolphin + build: standalone + kind: fixed_name + names: + - gba_bios.bin + case_insensitive: false + search_dir: $bios_path + passes_to: "Dolphin.ini [GBA] BIOS" + fails_when_missing: false + source_ref: "dolphin/component_prepare.sh:14, 50" +- emulator: duckstation + build: standalone + kind: passes_path + names: [] + search_dir: $bios_path + passes_to: "settings.ini [BIOS] SearchDirectory" + fails_when_missing: false + source_ref: "duckstation/component_prepare.sh:15, 33" +- emulator: gzdoom + build: standalone + kind: passes_path + names: [] + search_dir: $bios_path/gzdoom + passes_to: "gzdoom.ini [FileSearch.Directories] Path" + fails_when_missing: false + source_ref: "gzdoom/rd_assets/rd_config/gzdoom.ini:19, gzdoom/component_prepare.sh:23,\ + \ 28" +- emulator: mame + build: standalone + kind: passes_path + names: [] + search_dir: $bios_path/mame-sa/samples + passes_to: mame.ini samplepath + fails_when_missing: false + source_ref: "mame/component_prepare.sh:94, 117" +- emulator: melonds + build: standalone + kind: links + names: [] + search_dir: $bios_path + passes_to: $XDG_CONFIG_HOME/melonDS/bios is a symlink to it + fails_when_missing: false + source_ref: "melonds/component_prepare.sh:19, 41" +- emulator: melonds + build: standalone + kind: fixed_name + names: + - bios9.bin + - bios7.bin + - firmware.bin + - dsi_nand.bin + - dsi_bios7.bin + - dsi_bios9.bin + - dsi_firmware.bin + case_insensitive: false + search_dir: $bios_path + passes_to: "melonDS.toml [DS] BIOS9Path, [DS] BIOS7Path, [DS] FirmwarePath, [DSi]\ + \ NANDPath, [DSi] BIOS7Path, [DSi] BIOS9Path, [DSi] FirmwarePath" + fails_when_missing: false + source_ref: "melonds/component_prepare.sh:21-23, 25-28, 43-45, 47-50" +- emulator: pcsx2 + build: standalone + kind: passes_path + names: [] + search_dir: $bios_path + passes_to: "PCSX2.ini [Folders] Bios" + fails_when_missing: false + source_ref: "pcsx2/component_prepare.sh:16, 80" +- emulator: pico-8 + build: standalone + kind: links + names: [] + search_dir: $bios_path/pico-8 + passes_to: $HOME/.lexaloffle/pico-8 is a symlink to it + fails_when_missing: false + source_ref: "pico-8/component_prepare.sh:15" +- emulator: pico-8 + build: standalone + kind: fixed_name + names: + - pico8 + case_insensitive: false + search_dir: $bios_path/pico-8 + passes_to: exec + fails_when_missing: true + source_ref: "pico-8/component_launcher.sh:3" +- emulator: ppsspp + build: standalone + kind: copies + names: [] + search_dir: $component_extras/ppsspp_foss_bios.tar.gz + passes_to: $bios_path/PPSSPP + fails_when_missing: false + source_ref: "ppsspp/component_prepare.sh:38" +- emulator: primehack + build: standalone + kind: links + slot: EUR + names: [] + search_dir: $saves_path/gc/primehack/EU + passes_to: $XDG_DATA_HOME/primehack/GC/EUR is a symlink to it + fails_when_missing: false + source_ref: "primehack/component_prepare.sh:15, 33" +- emulator: primehack + build: standalone + kind: links + slot: USA + names: [] + search_dir: $saves_path/gc/primehack/US + passes_to: $XDG_DATA_HOME/primehack/GC/USA is a symlink to it + fails_when_missing: false + source_ref: "primehack/component_prepare.sh:16, 34" +- emulator: primehack + build: standalone + kind: links + slot: JAP + names: [] + search_dir: $saves_path/gc/primehack/JP + passes_to: $XDG_DATA_HOME/primehack/GC/JAP is a symlink to it + fails_when_missing: false + source_ref: "primehack/component_prepare.sh:17, 35" +- emulator: retroarch + build: libretro + kind: copies + names: [] + search_dir: $retroarch_extras_path/mame2003-plus.tar.gz + passes_to: $bios_path/mame2003-plus + fails_when_missing: false + source_ref: "retroarch/component_prepare.sh:86" +- emulator: retroarch + build: libretro + kind: links + names: [] + search_dir: $bios_path + passes_to: $bios_path/pcsx2/bios is a symlink to it + fails_when_missing: false + source_ref: "retroarch/component_prepare.sh:96" +- emulator: retroarch + build: libretro + kind: passes_path + names: [] + search_dir: $bios_path + passes_to: retroarch.cfg system_directory + fails_when_missing: false + source_ref: "retroarch/component_prepare.sh:44, 216" +- emulator: retroarch + build: libretro + kind: copies + names: + - cannonball + - dinothawr + - DirkSimple + - dolphin-emu + - ecwolf.pk3 + - nxengine + - PPSSPP + - prboom.wad + - qemu + - xrick + - mame2003 + - mame2003-plus + - pcsx2 + - fbneo + search_dir: $retroarch_extras_path + passes_to: $bios_path/ + fails_when_missing: false + source_ref: "retroarch/component_prepare.sh:58-67, 70, 87, 95, 109" +- emulator: retroarch + build: libretro + kind: copies + names: + - Databases + - Machines + search_dir: $retroarch_extras_path/MSX + passes_to: $bios_path/ + fails_when_missing: false + source_ref: "retroarch/component_prepare.sh:122, 124" +- emulator: retroarch + build: libretro + kind: copies + names: + - capsimg.so + search_dir: $retroarch_extras_path/Amiga + passes_to: $bios_path/ + fails_when_missing: false + source_ref: "retroarch/component_prepare.sh:131" +- emulator: retroarch + build: libretro + kind: copies + names: + - scummvm.ini + search_dir: $component_config + passes_to: $bios_path/ + fails_when_missing: false + source_ref: "retroarch/component_prepare.sh:137" +- emulator: rpcs3 + build: standalone + kind: fixed_name + names: + - PS3UPDAT.PUP + case_insensitive: false + search_dir: $roms_path/ps3/tmp + passes_to: component_launcher.sh --installfw + fails_when_missing: false + depends_on_option: "update_rpcs3_firmware downloads http://dus01.ps3.update.playstation.net/update/ps3/image/us/2026_0318_a2b60b6ac1d2e49e230144345616927c/PS3UPDAT.PUP" + source_ref: "rpcs3/component_functions.sh:11, 19-20" +- emulator: vita3k + build: standalone + kind: fixed_name + names: + - PSVUPDAT.PUP + case_insensitive: false + search_dir: /tmp + passes_to: component_launcher.sh --firmware + fails_when_missing: false + depends_on_option: "update_vita3k_firmware downloads http://dus01.psv.update.playstation.net/update/psv/image/2022_0209/rel_f2c7b12fe85496ec88a0391b514d6e3b/PSVUPDAT.PUP" + source_ref: "vita3k/component_functions.sh:5, 18, 20" +- emulator: vita3k + build: standalone + kind: fixed_name + names: + - PSP2UPDAT.PUP + case_insensitive: false + search_dir: /tmp + passes_to: component_launcher.sh --firmware + fails_when_missing: false + depends_on_option: "update_vita3k_firmware downloads http://dus01.psp2.update.playstation.net/update/psp2/image/2019_0924/sd_8b5f60b56c3da8365b973dba570c53a5/PSP2UPDAT.PUP?dest=us" + source_ref: "vita3k/component_functions.sh:6, 19, 21" +- emulator: xemu + build: standalone + kind: fixed_name + names: + - mcpx_1.0.bin + - Complex.bin + - xbox_hdd.qcow2 + case_insensitive: false + search_dir: $bios_path + passes_to: "xemu.toml [sys.files] bootrom_path, [sys.files] flashrom_path, [sys.files]\ + \ hdd_path" + fails_when_missing: false + source_ref: "xemu/component_prepare.sh:23-24, 27, 43-44, 46" +- emulator: xemu + build: standalone + kind: copies + names: + - xbox_hdd.qcow2 + search_dir: $component_extras + passes_to: $bios_path/ + fails_when_missing: false + depends_on_option: "[ ! -f \"$bios_path/xbox_hdd.qcow2\" ]" + source_ref: "xemu/component_prepare.sh:32" +- emulator: xroar + build: standalone + kind: passes_path + names: [] + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: xroar.conf rompath + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:290, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: dragon + names: + - dragon + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist dragon + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:291, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: d64_1 + names: + - d64_1 + - d64rom1 + - Dragon Data Ltd - Dragon 64 - IC17 + - Dragon Data Ltd - TANO IC18 + - Eurohard S.A. - Dragon 200 IC18 + - dragrom + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist d64_1 + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:292, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: d64_2 + names: + - d64_2 + - d64rom2 + - Dragon Data Ltd - Dragon 64 - IC18 + - Dragon Data Ltd - TANO IC17 + - Eurohard S.A. - Dragon 200 IC17 + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist d64_2 + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:293, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: d32 + names: + - d32 + - dragon32 + - d32rom + - Dragon Data Ltd - Dragon 32 - IC17 + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist d32 + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:294, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: d200e_1 + names: + - d200e_1 + - d200e_rom1 + - ic18_v1.4e.ic34 + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist d200e_1 + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:295, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: d200e_2 + names: + - d200e_2 + - d200e_rom2 + - ic17_v1.4e.ic37 + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist d200e_2 + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:296, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: dragon64 + names: + - d64_1 + - d64rom1 + - Dragon Data Ltd - Dragon 64 - IC17 + - Dragon Data Ltd - TANO IC18 + - Eurohard S.A. - Dragon 200 IC18 + - dragrom + - dragon + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist dragon64 + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:297, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: dragon64_alt + names: + - d64_2 + - d64rom2 + - Dragon Data Ltd - Dragon 64 - IC18 + - Dragon Data Ltd - TANO IC17 + - Eurohard S.A. - Dragon 200 IC17 + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist dragon64_alt + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:298, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: dragon32 + names: + - d32 + - dragon32 + - d32rom + - Dragon Data Ltd - Dragon 32 - IC17 + - dragon + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist dragon32 + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:299, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: dragon200e + names: + - d200e_1 + - d200e_rom1 + - ic18_v1.4e.ic34 + - d64_1 + - d64rom1 + - Dragon Data Ltd - Dragon 64 - IC17 + - Dragon Data Ltd - TANO IC18 + - Eurohard S.A. - Dragon 200 IC18 + - dragrom + - dragon + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist dragon200e + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:300, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: dragon200e_alt + names: + - d200e_2 + - d200e_rom2 + - ic17_v1.4e.ic37 + - d64_2 + - d64rom2 + - Dragon Data Ltd - Dragon 64 - IC18 + - Dragon Data Ltd - TANO IC17 + - Eurohard S.A. - Dragon 200 IC17 + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist dragon200e_alt + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:301, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: dragon200e_charset + names: + - d200e_26 + - rom26.ic1 + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist dragon200e_charset + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:302, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: dragonpro_boot + names: + - alpha-boot-v1.0 + - alpha_bt_10 + - Dragon Data Ltd - Dragon Alpha Boot v1.0 150584 + - alpha-boot-v0.4 + - alpha_bt_04 + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist dragonpro_boot + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:303, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: dragonpro_basic + names: + - alpha-basic + - alpha_ba + - Dragon Data Ltd - Dragon Alpga Basic + - d64_1 + - d64rom1 + - Dragon Data Ltd - Dragon 64 - IC17 + - Dragon Data Ltd - TANO IC18 + - Eurohard S.A. - Dragon 200 IC18 + - dragrom + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist dragonpro_basic + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:304, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: coco + names: + - bas13 + - bas12 + - "Color Basic v1.2 (1982)(Tandy)" + - bas11 + - bas10 + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist coco + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:305, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: coco_ext + names: + - extbas11 + - extbas10 + - coco + - COCO + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist coco_ext + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:306, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: coco1 + names: + - bas10 + - bas13 + - bas12 + - "Color Basic v1.2 (1982)(Tandy)" + - bas11 + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist coco1 + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:307, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: coco1e + names: + - bas11 + - bas13 + - bas12 + - "Color Basic v1.2 (1982)(Tandy)" + - bas10 + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist coco1e + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:308, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: coco1e_ext + names: + - extbas10 + - extbas11 + - coco + - COCO + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist coco1e_ext + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:309, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: coco2 + names: + - bas12 + - bas13 + - "Color Basic v1.2 (1982)(Tandy)" + - bas11 + - bas10 + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist coco2 + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:310, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: coco2_ext + names: + - extbas11 + - extbas10 + - coco + - COCO + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist coco2_ext + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:311, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: coco2b + names: + - bas13 + - bas12 + - "Color Basic v1.2 (1982)(Tandy)" + - bas11 + - bas10 + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist coco2b + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:312, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: deluxecoco + names: + - adv070_u24 + - deluxe + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist deluxecoco + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:313, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: deluxecoco1 + names: + - adv071_u24 + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist deluxecoco1 + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:314, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: deluxecoco2 + names: + - adv072_u24 + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist deluxecoco2 + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:315, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: deluxecoco3 + names: + - adv073-2_u24 + - adv073_u24 + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist deluxecoco3 + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:316, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: mx1600 + names: + - mx1600bas + - mx1600bas_zephyr + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist mx1600 + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:317, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: mx1600ext + names: + - mx1600extbas + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist mx1600ext + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:318, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: coco3 + names: + - coco3 + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist coco3 + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:319, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: coco3p + names: + - coco3p + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist coco3p + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:320, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: glenside_ide + names: + - yados + - hdblba + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist glenside_ide + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:321, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: mc10 + names: + - mc10 + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist mc10 + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:322, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: alice + names: + - alice + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist alice + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:323, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: dragondos + names: + - ddos12a + - ddos12 + - ddos40 + - ddos15 + - ddos10 + - Dragon Data Ltd - DragonDOS 1.0 + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist dragondos + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:324, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: dosplus + names: + - dplus49b + - dplus48 + - dosplus-4.8 + - DOSPLUS + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist dosplus + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:325, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: superdos + names: + - sdose6 + - PNP - SuperDOS E6 + - sdose5 + - sdose4 + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist superdos + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:326, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: cumana + names: + - cdos20 + - CDOS20 + - CumanaDOSv2.0 + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist cumana + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:327, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: dragondos_compat + names: + - dplus49b + - dplus48 + - dosplus-4.8 + - DOSPLUS + - sdose6 + - PNP - SuperDOS E6 + - sdose5 + - sdose4 + - ddos12a + - ddos12 + - ddos40 + - ddos15 + - ddos10 + - Dragon Data Ltd - DragonDOS 1.0 + - cdos20 + - CDOS20 + - CumanaDOSv2.0 + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist dragondos_compat + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:328, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: rsdos + names: + - disk11 + - disk10 + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist rsdos + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:329, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: delta + names: + - delta2 + - delta1a + - delta19 + - delta + - deltados + - Premier Micros - DeltaDOS + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist delta + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:330, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: rsdos_becker + names: + - hdbdw3bck + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist rsdos_becker + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:331, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: mcx128 + names: + - mcx128 + - MCX_Basic_21 + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist mcx128 + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:332, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: xroar + build: standalone + kind: names_searched + slot: mcx128_azerty + names: + - mcx128a + - MCX_Basic_21_AZERTY + search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" + passes_to: romlist mcx128_azerty + fails_when_missing: false + source_ref: "xroar/rd_assets/rd_config/xroar.conf:333, xroar/component_prepare.sh:13,\ + \ 15" +- emulator: retroarch + build: libretro + kind: core_option + names: + - scph5500.bin + passes_to: core option duckstation_BIOS.PathNTSCJ + fails_when_missing: false + source_ref: "retroarch/rd_assets/rd_config/retroarch-core-options.cfg:165, retroarch/component_prepare.sh:24" +- emulator: retroarch + build: libretro + kind: core_option + names: + - scph5501.bin + passes_to: core option duckstation_BIOS.PathNTSCU + fails_when_missing: false + source_ref: "retroarch/rd_assets/rd_config/retroarch-core-options.cfg:166, retroarch/component_prepare.sh:24" +- emulator: retroarch + build: libretro + kind: core_option + names: + - scph5502.bin + passes_to: core option duckstation_BIOS.PathPAL + fails_when_missing: false + source_ref: "retroarch/rd_assets/rd_config/retroarch-core-options.cfg:167, retroarch/component_prepare.sh:24" +- emulator: retroarch + build: libretro + kind: core_option + names: + - scph10000.bin + passes_to: core option pcsx2_bios + fails_when_missing: false + source_ref: "retroarch/rd_assets/rd_config/retroarch-core-options.cfg:515, retroarch/component_prepare.sh:24" diff --git a/scripts/scraper/retrodeck_launchers.py b/scripts/scraper/retrodeck_launchers.py new file mode 100644 index 00000000..fb838c1b --- /dev/null +++ b/scripts/scraper/retrodeck_launchers.py @@ -0,0 +1,498 @@ +"""RetroDECK's launcher layer, read from each component's scripts. + +A component's `component_prepare.sh` writes the emulator's config on reset: +melonDS gets seven fixed paths under $bios_path, xemu three, DuckStation and +PCSX2 a directory to search. It links folders the emulator reads into or out +of bios/ (LRPS2's pcsx2/bios points back at bios/, Dolphin's GC/ +folders live under saves/), and copies package-provided trees into bios/. +The presets it copies carry the rest: RetroArch core options that name a +BIOS file, XRoar's ROM search lists. RPCS3's firmware is downloaded by +RetroDECK itself and PICO-8 is started from a binary the user drops in bios/. + +The shell is read as text, never run. Every line of a prepare or launcher +script that names $bios_path must match a known construct, else the scrape +stops with LauncherPatternMissing: an unclassified use would vanish from the +platform file without a word. +""" + +from __future__ import annotations + +import re +import shlex +from collections.abc import Callable +from pathlib import PurePosixPath +from typing import Any + +try: + from .retropie_launchers import Launcher, LauncherPatternMissing, merge_records, source_ref +except ImportError: + from retropie_launchers import Launcher, LauncherPatternMissing, merge_records, source_ref + +BIOS = "$bios_path" +PLACEHOLDER = "RETRODECKBIOSDIR" +CORE_OPTIONS = "retroarch/rd_assets/rd_config/retroarch-core-options.cfg" +# The component that runs libretro cores. +LIBRETRO_COMPONENT = "retroarch" +# Values of a core option that name a file rather than a setting. +_FILE_VALUE = re.compile(r"\.(bin|rom|img|zip|bios)$", re.IGNORECASE) +_EXTENSION = re.compile(r"\.[A-Za-z0-9]{1,6}$") +_ASSIGN = re.compile(r'^([A-Za-z_]\w*)="([^"]*)"$') +_GC_REGION = re.compile(r"/GC/(EUR|USA|JAP)$") + +# Commands whose $bios_path argument creates or backs up a folder, logs, or +# only tests a path. +HOUSEKEEPING = frozenset({"create_dir", "log", "mkdir", "chmod", "rm", "unlink"}) + +Read = Callable[[str], "str | None"] + + +def _words(text: str) -> list[str]: + try: + return shlex.split(text, comments=True) + except ValueError: + return text.split() + + +def _unquote(value: str) -> str: + return value.strip().strip("'\"") + + +def _split_path(path: str) -> tuple[str, str]: + pure = PurePosixPath(path.rstrip("/")) + return str(pure.parent), pure.name + + +def _is_file(path: str) -> bool: + return bool(_EXTENSION.search(PurePosixPath(path).name)) + + +def _mentions_bios(text: str) -> bool: + return BIOS in text or "${bios_path}" in text + + +class Script: + """One component shell script, line by line, with its enclosing tests.""" + + def __init__(self, path: str, text: str) -> None: + self.path = path + self.lines = text.splitlines() + + def statements(self) -> list[tuple[int, list[str], str, list[str]]]: + """(line, words, raw, enclosing conditions) for each command, `&&` chains split.""" + found = [] + branches: list[str] = [] + for number, line in enumerate(self.lines, 1): + stripped = line.strip() + if not stripped or stripped.startswith("#"): + continue + opened = re.match(r"^(if|elif)\s+(.*?)\s*;\s*then\s*$", stripped) + if opened is not None: + condition = opened.group(2) + if opened.group(1) == "if": + branches.append(condition) + elif branches: + branches[-1] = condition + continue + if stripped == "else" and branches: + branches[-1] = f"not ({branches[-1]})" + continue + if stripped == "fi": + if branches: + branches.pop() + continue + shown = [c for c in branches if "$action" not in c] + for part in re.split(r"\s+&&\s+|\s+\|\|\s+", stripped): + words = _words(part) + if words: + found.append((number, words, part, shown)) + return found + + +class Component: + """What one component's scripts and presets hand its emulator.""" + + def __init__(self, name: str, read: Read) -> None: + self.name = name + self.read = read + self.build = "libretro" if name == LIBRETRO_COMPONENT else "standalone" + self.records: list[Launcher] = [] + self.variables = self._variables() + + def _variables(self) -> dict[str, str]: + text = self.read(f"{self.name}/component_functions.sh") or "" + return { + m.group(1): m.group(2) + for m in (_ASSIGN.match(line.strip()) for line in text.splitlines()) + if m is not None + } + + def resolve(self, word: str) -> str: + """A word with the component's own bios/ paths filled in (`$retroarch_config_scummvm`).""" + def fill(found: re.Match[str]) -> str: + value = self.variables.get(found.group(1) or found.group(2), "") + return value if _mentions_bios(value) else found.group(0) + + return re.sub(r"\$\{(\w+)\}|\$(\w+)", fill, word) + + def config_name(self, value: str) -> str: + """`melonDS.toml` for `$melonds_config`, as component_functions.sh defines it.""" + variable = re.fullmatch(r"\$\{?(\w+)\}?", value) + if variable is None or variable.group(1) not in self.variables: + return value + return PurePosixPath(self.variables[variable.group(1)]).name + + def record( + self, kind: str, names: list[str], lines: list[tuple[str, int]], **fields: Any + ) -> None: + self.records.append(Launcher( + emulator=fields.pop("emulator", self.name), build=fields.pop("build", self.build), + kind=kind, names=names, source_ref=source_ref(lines), lines=list(lines), **fields, + )) + + def read_prepare(self) -> None: + path = f"{self.name}/component_prepare.sh" + text = self.read(path) + if text is None: + return + script = Script(path, text) + settings: dict[tuple[str, ...], list[tuple[int, str, str]]] = {} + copies: dict[tuple[str, str | None], list[tuple[int, str, str]]] = {} + for number, words, raw, conditions in script.statements(): + depends = "; ".join(conditions) or None + command = words[0] + if command != "set_setting_value": + words = [self.resolve(w) for w in words] + raw = " ".join(words) + if command == "set_setting_value" and len(words) >= 4: + value = _unquote(words[3]) + if _mentions_bios(value): + section = words[5] if len(words) > 5 else None + where = f"[{section}] {words[2]}" if section else words[2] + target = self.config_name(words[1]) + if _is_file(value): + settings.setdefault(("fixed_name", target, depends or ""), []).append( + (number, value, where) + ) + else: + key = ("passes_path", target, where, depends or "") + settings.setdefault(key, []).append((number, value, where)) + continue + if command == "dir_prep" and len(words) == 3: + self._dir_prep(path, number, words[1], words[2], depends) + continue + if command == "ln" and _mentions_bios(raw): + self._link(path, number, words, depends) + continue + if command == "cp" and len(words) >= 3 and _mentions_bios(words[-1]): + source, target = words[-2], words[-1] + folder, name = _split_path(source) + copies.setdefault((folder, depends), []).append((number, name, target)) + continue + if command == "tar" and "-C" in words and _mentions_bios(words[words.index("-C") + 1]): + if "-czf" in words: + continue + archive = next((w for w in words if w.endswith((".tar.gz", ".tgz", ".tar"))), None) + if archive is None: + raise LauncherPatternMissing( + f"{path}:{number}", f"tar into bios/ of no archive: {raw}" + ) + self.record( + "copies", [], [(path, number)], search_dir=archive, + passes_to=words[words.index("-C") + 1], fails_when_missing=False, + depends_on_option=depends, + ) + continue + if command == "sed" and PLACEHOLDER in raw: + self._preset(script, number, words) + continue + if not _mentions_bios(raw): + continue + if command in HOUSEKEEPING or command in ("[[", "[", "if") or command.startswith("[["): + continue + raise LauncherPatternMissing( + f"{path}:{number}", f"unrecognised use of $bios_path: {raw}" + ) + for key, hits in settings.items(): + self._settings(path, key, hits) + for (folder, depends), hits in copies.items(): + names = [name for _n, name, _t in hits] + targets = [target for _n, _name, target in hits] + self.record( + "copies", names, [(path, n) for n, _name, _t in hits], search_dir=folder, + passes_to=_render_targets(names, targets), fails_when_missing=False, + depends_on_option=depends or None, + ) + + def _settings(self, path: str, key: tuple[str, ...], hits: list[tuple[int, str, str]]) -> None: + kind, target = key[0], key[1] + depends = key[-1] or None + lines = [(path, n) for n, _v, _w in hits] + if kind == "fixed_name": + folders = {_split_path(value)[0] for _n, value, _w in hits} + if len(folders) != 1: + raise LauncherPatternMissing( + f"{path}:{hits[0][0]}", "fixed names in several folders" + ) + names = list(dict.fromkeys(_split_path(value)[1] for _n, value, _w in hits)) + wheres = list(dict.fromkeys(w for _n, _v, w in hits)) + self.record( + "fixed_name", names, lines, + case_insensitive=False, search_dir=folders.pop(), + passes_to=f"{target} " + ", ".join(wheres), + fails_when_missing=False, depends_on_option=depends, + ) + return + values = {v for _n, v, _w in hits} + if len(values) != 1: + raise LauncherPatternMissing(f"{path}:{hits[0][0]}", f"{key[2]} set to several folders") + self.record( + "passes_path", [], lines, search_dir=values.pop(), + passes_to=f"{target} {key[2]}", fails_when_missing=False, depends_on_option=depends, + ) + + def _dir_prep(self, path: str, number: int, real: str, link: str, depends: str | None) -> None: + """`dir_prep REAL LINK` makes LINK a symlink to REAL (functions/other_functions.sh).""" + region = _GC_REGION.search(link) + if region is not None: + self.record( + "links", [], [(path, number)], slot=region.group(1), search_dir=real, + passes_to=f"{link} is a symlink to it", fails_when_missing=False, + depends_on_option=depends, + ) + return + if real.startswith(BIOS): + self.record( + "links", [], [(path, number)], search_dir=real, + passes_to=f"{link} is a symlink to it", fails_when_missing=False, + depends_on_option=depends, + ) + # A user folder (saves, textures, cheats) linked into bios/ hands no BIOS over. + + def _link(self, path: str, number: int, words: list[str], depends: str | None) -> None: + paths = [w for w in words[1:] if not w.startswith("-")] + if len(paths) != 2 or not _mentions_bios(paths[0]): + raise LauncherPatternMissing( + f"{path}:{number}", f"link without a bios/ source: {words}" + ) + folder, name = _split_path(paths[0]) + self.record( + "links", [name], [(path, number)], case_insensitive=False, search_dir=folder, + passes_to=f"{paths[1]} is a symlink to it", fails_when_missing=False, + depends_on_option=depends, + ) + + def _preset(self, script: Script, number: int, words: list[str]) -> None: + """`sed -i "s#RETRODECKBIOSDIR#${bios_path}#g" "$cfg"` on a preset copied just above.""" + target = words[-1] + substitutions = self._substitutions(script, target) + copied = None + for line_no, line in enumerate(script.lines[:number], 1): + cp = _words(line.strip().split("&&")[0]) + if cp and cp[0] == "cp" and cp[-1] == target and "$component_config/" in cp[-2]: + copied = (line_no, cp[-2].split("$component_config/", 1)[1]) + if copied is None: + raise LauncherPatternMissing(f"{script.path}:{number}", f"no preset copied to {target}") + preset = f"{self.name}/rd_assets/rd_config/{copied[1]}" + text = self.read(preset) + if text is None: + raise LauncherPatternMissing(preset, "preset is missing") + section = None + found = 0 + for line_no, line in enumerate(text.splitlines(), 1): + stripped = line.strip() + if re.fullmatch(r"\[[^\]]+\]", stripped): + section = stripped + if PLACEHOLDER not in stripped or stripped.startswith("#"): + continue + setting = re.match(r"^([\w.$()-]+)\s*(?:=|\s)\s*(.+)$", stripped) + if setting is None: + raise LauncherPatternMissing(f"{preset}:{line_no}", f"placeholder in {stripped}") + key, value = setting.group(1), _unquote(setting.group(2)) + for placeholder, variable in substitutions.items(): + value = value.replace(placeholder, variable) + where = f"{section} {key}" if section else key + found += 1 + applied = [(script.path, copied[0]), (script.path, number)] + self.record( + "passes_path", [], [(preset, line_no)] + applied, + search_dir=value, passes_to=f"{self.config_name(target)} {where}", + fails_when_missing=False, + ) + if key == "rompath": + self.romlists(preset, text, value, applied) + if not found: + raise LauncherPatternMissing(preset, f"no {PLACEHOLDER} line") + + @staticmethod + def _substitutions(script: Script, target: str) -> dict[str, str]: + found = {} + for line in script.lines: + for m in re.finditer(r"s#(RETRODECK\w+)#\$\{?(\w+)\}?#g[\"']?\s+\"([^\"]+)\"", line): + if m.group(3) == target: + found[m.group(1)] = f"${m.group(2)}" + return found + + def romlists( + self, preset: str, text: str, rompath: str, applied: list[tuple[str, int]] + ) -> None: + """XRoar's `romlist NAME=a,b,@other`: the names tried for each ROM, in order.""" + lists: dict[str, tuple[int, list[str]]] = {} + for line_no, line in enumerate(text.splitlines(), 1): + found = re.match(r"^romlist\s+([\w-]+)=(.*)$", line.strip()) + if found is not None: + items = [_unquote(i) for i in re.findall(r'"[^"]*"|[^,]+', found.group(2))] + lists[found.group(1)] = (line_no, items) + if not lists: + raise LauncherPatternMissing(preset, "rompath with no romlist") + + def expand(name: str, seen: tuple[str, ...]) -> list[str]: + names = [] + for item in lists[name][1]: + if item.startswith("@"): + ref = item[1:] + if ref not in lists or ref in seen: + raise LauncherPatternMissing(preset, f"romlist {name} refers to {item}") + names += expand(ref, seen + (ref,)) + else: + names.append(item) + return list(dict.fromkeys(names)) + + for name, (line_no, _items) in lists.items(): + self.record( + "names_searched", expand(name, (name,)), [(preset, line_no)] + applied, + slot=name, search_dir=rompath, passes_to=f"romlist {name}", + fails_when_missing=False, + ) + + def read_launcher(self) -> None: + path = f"{self.name}/component_launcher.sh" + text = self.read(path) + if text is None: + return + for number, words, raw, conditions in Script(path, text).statements(): + if not _mentions_bios(raw): + continue + if words[0] == "exec" and len(words) > 1 and _mentions_bios(words[1]): + folder, name = _split_path(words[1]) + self.record( + "fixed_name", [name], [(path, number)], case_insensitive=False, + search_dir=folder, passes_to="exec", fails_when_missing=True, + depends_on_option="; ".join(conditions) or None, + ) + continue + raise LauncherPatternMissing( + f"{path}:{number}", f"unrecognised use of $bios_path: {raw}" + ) + + def read_firmware_download(self) -> None: + """A firmware URL RetroDECK downloads and hands to the emulator's installer.""" + path = f"{self.name}/component_functions.sh" + text = self.read(path) + if text is None: + return + urls = { + name: value for name, value in self.variables.items() + if re.match(r"https?://", value) + } + script = Script(path, text) + statements = script.statements() + for number, words, _raw, _conditions in statements: + if words[0] != "download_file" or len(words) < 3: + continue + source = re.fullmatch(r"\$\{?(\w+)\}?", words[1]) + if source is None or source.group(1) not in urls: + continue + destination = words[2] + installs = [ + (n, w) for n, w, _r, _c in statements + if destination in w and w[0] != "download_file" + and any(_installs(f) for f in w) + ] + if not installs: + raise LauncherPatternMissing( + f"{path}:{number}", f"{destination} is downloaded but never installed" + ) + install_line, install = installs[0] + flag = next(f for f in install if _installs(f)) + function = self._function_at(script, number) + assigned = next( + n for n, line in enumerate(script.lines, 1) + if line.strip().startswith(f"{source.group(1)}=") + ) + folder, name = _split_path(destination) + self.record( + "fixed_name", [name], [(path, assigned), (path, number), (path, install_line)], + case_insensitive=False, search_dir=folder, + passes_to=f"component_launcher.sh {flag}", fails_when_missing=False, + depends_on_option=f"{function} downloads {urls[source.group(1)]}", + ) + + @staticmethod + def _function_at(script: Script, number: int) -> str: + for line in reversed(script.lines[:number]): + found = re.match(r"^([\w-]+)\s*\(\)\s*\{", line) + if found is not None: + return found.group(1) + raise LauncherPatternMissing(f"{script.path}:{number}", "download outside a function") + + +def _installs(word: str) -> bool: + """A launcher flag that installs a firmware file: RPCS3 --installfw, Vita3K --firmware.""" + return word.startswith("--install") or word == "--firmware" + + +def _render_targets(names: list[str], targets: list[str]) -> str: + """Where copied names land: one folder when each keeps its name, else each path.""" + folders = {_split_path(t)[0] for t in targets} + kept = [_split_path(t)[1] for t in targets] == [PurePosixPath(n).name for n in names] + if kept and len(folders) == 1: + return f"{folders.pop()}/" + return ", ".join(targets) + + +def core_options(read: Read) -> list[Launcher]: + """RetroArch core options in RetroDECK's preset whose value names a BIOS file.""" + text = read(CORE_OPTIONS) + prepare_path = f"{LIBRETRO_COMPONENT}/component_prepare.sh" + prepare = read(prepare_path) or "" + if text is None: + raise LauncherPatternMissing(CORE_OPTIONS, "preset is missing") + copied = [ + n for n, line in enumerate(prepare.splitlines(), 1) + if "retroarch-core-options.cfg" in line and line.strip().startswith("cp ") + ] + if not copied: + raise LauncherPatternMissing(prepare_path, "the core options preset is never copied") + records = [] + for line_no, line in enumerate(text.splitlines(), 1): + found = re.match(r'^([\w.-]+)\s*=\s*"([^"]*)"$', line.strip()) + if found is None or not _FILE_VALUE.search(found.group(2)): + continue + key, value = found.group(1), found.group(2) + records.append(Launcher( + emulator=LIBRETRO_COMPONENT, build="libretro", kind="core_option", names=[value], + passes_to=f"core option {key}", fails_when_missing=False, + source_ref=source_ref([(CORE_OPTIONS, line_no), (prepare_path, copied[0])]), + )) + if not records: + raise LauncherPatternMissing(CORE_OPTIONS, "no core option names a file") + return records + + +def extract_launchers(components: list[str], read: Read) -> list[dict[str, Any]]: + """Every launcher record of the given components, read through `read(repo path)`. + + `read` returns None only for a file the component does not have. + """ + records: list[Launcher] = [] + for name in components: + component = Component(name, read) + component.read_prepare() + component.read_launcher() + component.read_firmware_download() + records += merge_records(component.records) + if LIBRETRO_COMPONENT in components: + records += core_options(read) + if not records: + raise LauncherPatternMissing("components", "no component hands a file to its emulator") + return [record.entry() for record in records] diff --git a/scripts/scraper/retrodeck_scraper.py b/scripts/scraper/retrodeck_scraper.py index adb3ca1b..59c18443 100644 --- a/scripts/scraper/retrodeck_scraper.py +++ b/scripts/scraper/retrodeck_scraper.py @@ -40,16 +40,20 @@ try: BaseScraper, BiosRequirement, fetch_branch_head, + github_headers, requirement_entry, ) + from .retrodeck_launchers import extract_launchers except ImportError: sys.path.insert(0, str(Path(__file__).parent.parent)) from scraper.base_scraper import ( BaseScraper, BiosRequirement, fetch_branch_head, + github_headers, requirement_entry, ) + from scraper.retrodeck_launchers import extract_launchers PLATFORM_NAME = "retrodeck" COMPONENTS_REPO = "RetroDECK/components" @@ -233,6 +237,7 @@ class Scraper(BaseScraper): super().__init__() self.manifests_dir = manifests_dir self._manifests: list[tuple[str, dict]] | None = None + self._files: dict[str, str | None] = {} # The manifests are read from main: one commit for the tree and for # every file, and that commit is the one recorded. self.revision = ( @@ -317,6 +322,32 @@ class Scraper(BaseScraper): print(f" WARNING: {mf}: {e}", file=sys.stderr) return manifests + def component_file(self, path: str) -> str | None: + """A file of the components repo at the scraped revision, None when it has none. + + Only a 404 says the file is absent: any other failure stops the + scrape, as for the manifests. + """ + if path in self._files: + return self._files[path] + if self.manifests_dir: + local = Path(self.manifests_dir) / path + text = local.read_text(encoding="utf-8") if local.is_file() else None + else: + url = f"{RAW_BASE.format(rev=self.revision)}/{path}" + try: + req = urllib.request.Request(url, headers=github_headers()) + with urllib.request.urlopen(req, timeout=30) as resp: + text = resp.read().decode("utf-8") + except urllib.error.HTTPError as e: + if e.code != 404: + raise ConnectionError(f"{url}: HTTP {e.code}") from e + text = None + except urllib.error.URLError as e: + raise ConnectionError(f"{url}: {e.reason}") from e + self._files[path] = text + return text + def validate_format(self, raw_data: str) -> bool: try: return isinstance(json.loads(raw_data), dict) @@ -478,6 +509,7 @@ class Scraper(BaseScraper): # its own, and retroarch is the one that runs cores. "standalone_cores": [c for c in cores if c != "retroarch"], "systems": systems, + "launchers": extract_launchers(cores, self.component_file), } diff --git a/tests/test_launchers_retrodeck.py b/tests/test_launchers_retrodeck.py new file mode 100644 index 00000000..a4af6844 --- /dev/null +++ b/tests/test_launchers_retrodeck.py @@ -0,0 +1,334 @@ +"""RetroDECK's launcher layer read from each component's scripts and presets. + +Each fixture copies the lines an extractor reads from RetroDECK/components +fcd0d72995d7aa2e741e2753ac6914025b2ff1ac, placed at the line numbers they +have there, so the source_ref a record carries is the one platforms/retrodeck.yml +cites. A construct that is gone or unknown must stop the scrape. +""" + +from __future__ import annotations + +import sys +import textwrap +import unittest +from pathlib import Path + +import yaml + +REPO_ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(REPO_ROOT / "scripts")) + +from scraper import retrodeck_launchers as rd # noqa: E402 + + +def at(first_line: int, text: str) -> str: + """Text placed so its first line is line `first_line` of the file.""" + return place((first_line, text)) + + +def place(*parts: tuple[int, str]) -> str: + """Excerpts each placed at the line it starts on in the real file.""" + lines: list[str] = [] + for first_line, text in parts: + excerpt = textwrap.dedent(text).lstrip("\n").splitlines() + if len(lines) >= first_line: + raise ValueError(f"excerpt at line {first_line} overlaps the one before") + lines += [""] * (first_line - 1 - len(lines)) + excerpt + return "\n".join(lines) + "\n" + + +MELONDS_FUNCTIONS = at(3, 'melonds_config="$XDG_CONFIG_HOME/melonDS/melonDS.toml"\n') + +MELONDS_PREPARE = at(7, """ + if [[ "$action" == "reset" ]]; then # Run reset-only commands + log i "----------------------" + log i "Resetting $component_name" + log i "----------------------" + + create_dir -d "$XDG_CONFIG_HOME/melonDS/" + cp -fv "$component_config/melonDS.toml" "$melonds_config" + + create_dir "$saves_path/nds/melonds" + create_dir "$states_path/nds/melonds" + create_dir "$cheats_path/MelonDS" + + dir_prep "$bios_path" "$XDG_CONFIG_HOME/melonDS/bios" + + set_setting_value "$melonds_config" "BIOS9Path" "\\"$bios_path/bios9.bin\\"" "melonds" "DS" + set_setting_value "$melonds_config" "BIOS7Path" "\\"$bios_path/bios7.bin\\"" "melonds" "DS" + set_setting_value "$melonds_config" "FirmwarePath" "\\"$bios_path/firmware.bin\\"" "melonds" "DS" + + set_setting_value "$melonds_config" "NANDPath" "\\"$bios_path/dsi_nand.bin\\"" "melonds" "DSi" + set_setting_value "$melonds_config" "BIOS7Path" "\\"$bios_path/dsi_bios7.bin\\"" "melonds" "DSi" + set_setting_value "$melonds_config" "BIOS9Path" "\\"$bios_path/dsi_bios9.bin\\"" "melonds" "DSi" + set_setting_value "$melonds_config" "FirmwarePath" "\\"$bios_path/dsi_firmware.bin\\"" "melonds" "DSi" + + set_setting_value "$melonds_config" "SaveFilePath" "\\"$saves_path/nds/melonds\\"" "melonds" "Instance0" + fi +""") + +XEMU_FUNCTIONS = at(3, 'xemu_conf="$XDG_CONFIG_HOME/xemu/xemu.toml"\n') + +XEMU_PREPARE = at(20, """ + cp -fv "$component_config/xemu.toml" "$xemu_conf" + create_dir "$screenshots_path/xemu" + set_setting_value "$xemu_conf" "screenshot_dir" "'$screenshots_path/xemu'" "xemu" "General" + set_setting_value "$xemu_conf" "bootrom_path" "'$bios_path/mcpx_1.0.bin'" "xemu" "sys.files" + set_setting_value "$xemu_conf" "flashrom_path" "'$bios_path/Complex.bin'" "xemu" "sys.files" + create_dir "$saves_path/xbox/xemu" + set_setting_value "$xemu_conf" "eeprom_path" "'$saves_path/xbox/xemu/xbox-eeprom.bin'" "xemu" "sys.files" + set_setting_value "$xemu_conf" "hdd_path" "'$bios_path/xbox_hdd.qcow2'" "xemu" "sys.files" + + + # Preparing HD dummy Image if the image is not found + if [ ! -f "$bios_path/xbox_hdd.qcow2" ];then + cp -f "$component_extras/xbox_hdd.qcow2" "$bios_path/xbox_hdd.qcow2" + fi +""") + +RETROARCH_PREPARE = place((23, """ + cp -fv "$component_config/retroarch.cfg" "$retroarch_config" + cp -fv "$component_config/retroarch-core-options.cfg" "$retroarch_config_core_options" +"""), (44, """ + set_setting_value "$retroarch_config" "system_directory" "$bios_path" "retroarch" +"""), (95, """ + cp -rf "$retroarch_extras_path/pcsx2" "$bios_path/pcsx2" + dir_prep "$bios_path" "$bios_path/pcsx2/bios" + dir_prep "$saves_path/ps2/retroarch-core/LRPS2/memcards" "$bios_path/pcsx2/memcards" +""")) + +CORE_OPTIONS = place((165, """ + duckstation_BIOS.PathNTSCJ = "scph5500.bin" + duckstation_BIOS.PathNTSCU = "scph5501.bin" + duckstation_BIOS.PathPAL = "scph5502.bin" +"""), (515, """ + pcsx2_bios = "scph10000.bin" + pcsx2_boot_bios = "disabled" +""")) + +DOLPHIN_PREPARE = at(19, """ + dir_prep "$saves_path/gc/dolphin/EU" "$XDG_DATA_HOME/dolphin-emu/GC/EUR" # TODO: Multi-user one-off + dir_prep "$saves_path/gc/dolphin/US" "$XDG_DATA_HOME/dolphin-emu/GC/USA" # TODO: Multi-user one-off + dir_prep "$saves_path/gc/dolphin/JP" "$XDG_DATA_HOME/dolphin-emu/GC/JAP" # TODO: Multi-user one-off + dir_prep "$screenshots_path" "$XDG_DATA_HOME/dolphin-emu/ScreenShots" +""") + +RPCS3_FUNCTIONS = at(11, """ + rpcs3_firmware="http://dus01.ps3.update.playstation.net/update/ps3/image/us/2026_0318_a2b60b6ac1d2e49e230144345616927c/PS3UPDAT.PUP" + + update_rpcs3_firmware() { + if [[ $(check_network_connectivity) == "true" ]]; then + configurator_generic_dialog "RetroDECK Configurator - Install: RPCS3 Firmware" "This tool will download the firmware." + ( + create_dir "$roms_path/ps3/tmp" + chmod 777 "$roms_path/ps3/tmp" + download_file "$rpcs3_firmware" "$roms_path/ps3/tmp/PS3UPDAT.PUP" "RPCS3 Firmware" + bash "$rpcs3_component_dir/component_launcher.sh" --installfw "$roms_path/ps3/tmp/PS3UPDAT.PUP" + rm -rf "$roms_path/ps3/tmp" + ) + fi + } +""") + +PICO8_LAUNCHER = at(3, 'exec "$bios_path/pico-8/pico8" -desktop_path "$screenshots_path" "$@"\n') + +XROAR_PREPARE = at(12, """ + create_dir -d "$XDG_CONFIG_HOME/xroar" + cp -f "$component_config/xroar.conf" "$xroar_config" && log i "Copied default xroar.conf to $xroar_config" + sed -i "s#RETRODECKROMSDIR#${roms_path}#g" "$xroar_config" && log i "Set ROMs directory in xroar.conf" + sed -i "s#RETRODECKBIOSDIR#${bios_path}#g" "$xroar_config" && log i "Set BIOS directory in xroar.conf" +""") + +XROAR_CONF = place((290, """ + rompath RETRODECKROMSDIR/coco:RETRODECKROMSDIR/dragon32:RETRODECKROMSDIR/tanodragon:RETRODECKBIOSDIR + romlist dragon=dragon + romlist d64_1=d64_1,d64rom1,"Dragon Data Ltd - Dragon 64 - IC17","Dragon Data Ltd - TANO IC18","Eurohard S.A. - Dragon 200 IC18",dragrom +"""), (297, """ + romlist dragon64=@d64_1,@dragon +""")) + + +def tree(**components: dict[str, str]) -> dict[str, str]: + return { + f"{name}/{path}": text + for name, files in components.items() + for path, text in files.items() + } + + +def melonds(prepare: str) -> dict[str, str]: + return tree(melonds={ + "component_prepare.sh": prepare, "component_functions.sh": MELONDS_FUNCTIONS, + }) + + +def launchers(files: dict[str, str], components: list[str]) -> list[dict]: + return rd.extract_launchers(components, files.get) + + +RETROARCH = { + "component_prepare.sh": RETROARCH_PREPARE, + "rd_assets/rd_config/retroarch-core-options.cfg": CORE_OPTIONS, +} + + +class FixedNames(unittest.TestCase): + def test_melonds_gets_seven_paths_in_the_order_written(self): + files = melonds(MELONDS_PREPARE) + fixed = next(r for r in launchers(files, ["melonds"]) if r["kind"] == "fixed_name") + self.assertEqual(fixed["names"], [ + "bios9.bin", "bios7.bin", "firmware.bin", + "dsi_nand.bin", "dsi_bios7.bin", "dsi_bios9.bin", "dsi_firmware.bin", + ]) + self.assertEqual(fixed["search_dir"], "$bios_path") + self.assertTrue( + fixed["passes_to"].startswith("melonDS.toml [DS] BIOS9Path, [DS] BIOS7Path") + ) + self.assertEqual(fixed["source_ref"], "melonds/component_prepare.sh:21-23, 25-28") + + def test_melonds_reads_bios_through_a_link(self): + files = melonds(MELONDS_PREPARE) + link = next(r for r in launchers(files, ["melonds"]) if r["kind"] == "links") + self.assertEqual(link["passes_to"], "$XDG_CONFIG_HOME/melonDS/bios is a symlink to it") + self.assertEqual(link["source_ref"], "melonds/component_prepare.sh:19") + + def test_xemu_writes_three_paths_and_copies_a_blank_disk(self): + files = tree(xemu={ + "component_prepare.sh": XEMU_PREPARE, "component_functions.sh": XEMU_FUNCTIONS, + }) + found = launchers(files, ["xemu"]) + fixed = next(r for r in found if r["kind"] == "fixed_name") + self.assertEqual(fixed["names"], ["mcpx_1.0.bin", "Complex.bin", "xbox_hdd.qcow2"]) + self.assertEqual(fixed["source_ref"], "xemu/component_prepare.sh:23-24, 27") + copy = next(r for r in found if r["kind"] == "copies") + self.assertEqual(copy["depends_on_option"], '[ ! -f "$bios_path/xbox_hdd.qcow2" ]') + self.assertEqual(copy["source_ref"], "xemu/component_prepare.sh:32") + + def test_pico8_starts_the_binary_the_user_supplies(self): + files = tree(**{"pico-8": {"component_launcher.sh": PICO8_LAUNCHER}}) + record = launchers(files, ["pico-8"])[0] + self.assertEqual((record["names"], record["search_dir"]), (["pico8"], "$bios_path/pico-8")) + self.assertIs(record["fails_when_missing"], True) + self.assertEqual(record["source_ref"], "pico-8/component_launcher.sh:3") + + def test_rpcs3_firmware_is_downloaded_then_installed(self): + files = tree(rpcs3={"component_functions.sh": RPCS3_FUNCTIONS}) + record = launchers(files, ["rpcs3"])[0] + self.assertEqual(record["names"], ["PS3UPDAT.PUP"]) + self.assertEqual(record["passes_to"], "component_launcher.sh --installfw") + self.assertIn("2026_0318_a2b60b6ac1d2e49e230144345616927c", record["depends_on_option"]) + self.assertEqual(record["source_ref"], "rpcs3/component_functions.sh:11, 19-20") + + def test_a_download_never_installed_raises(self): + broken = RPCS3_FUNCTIONS.replace("--installfw", "--run") + with self.assertRaises(rd.LauncherPatternMissing): + launchers(tree(rpcs3={"component_functions.sh": broken}), ["rpcs3"]) + + +class RetroArch(unittest.TestCase): + def test_core_options_that_name_a_file(self): + found = launchers(tree(retroarch=RETROARCH), ["retroarch"]) + found = [r for r in found if r["kind"] == "core_option"] + self.assertEqual( + [(r["names"], r["passes_to"]) for r in found], + [ + (["scph5500.bin"], "core option duckstation_BIOS.PathNTSCJ"), + (["scph5501.bin"], "core option duckstation_BIOS.PathNTSCU"), + (["scph5502.bin"], "core option duckstation_BIOS.PathPAL"), + (["scph10000.bin"], "core option pcsx2_bios"), + ], + ) + self.assertEqual( + found[3]["source_ref"], + "retroarch/rd_assets/rd_config/retroarch-core-options.cfg:515, " + "retroarch/component_prepare.sh:24", + ) + + def test_lrps2_bios_folder_points_back_at_bios(self): + found = launchers(tree(retroarch=RETROARCH), ["retroarch"]) + links = [r for r in found if r["kind"] == "links"] + self.assertEqual([(r["search_dir"], r["passes_to"]) for r in links], + [("$bios_path", "$bios_path/pcsx2/bios is a symlink to it")]) + self.assertEqual(links[0]["source_ref"], "retroarch/component_prepare.sh:96") + + def test_a_preset_never_copied_raises(self): + prepare = RETROARCH_PREPARE.replace("retroarch-core-options.cfg", "other.cfg") + files = tree(retroarch={**RETROARCH, "component_prepare.sh": prepare}) + with self.assertRaises(rd.LauncherPatternMissing): + launchers(files, ["retroarch"]) + + +class Links(unittest.TestCase): + def test_dolphin_reads_ipl_from_save_folders(self): + found = launchers(tree(dolphin={"component_prepare.sh": DOLPHIN_PREPARE}), ["dolphin"]) + self.assertEqual( + [(r["slot"], r["search_dir"]) for r in found], + [("EUR", "$saves_path/gc/dolphin/EU"), ("USA", "$saves_path/gc/dolphin/US"), + ("JAP", "$saves_path/gc/dolphin/JP")], + ) + self.assertEqual(found[1]["source_ref"], "dolphin/component_prepare.sh:20") + + +class Presets(unittest.TestCase): + def files(self, conf: str = XROAR_CONF) -> dict[str, str]: + return tree(xroar={ + "component_prepare.sh": XROAR_PREPARE, "rd_assets/rd_config/xroar.conf": conf, + }) + + def test_xroar_romlists_expand_their_references_in_order(self): + found = launchers(self.files(), ["xroar"]) + search = next(r for r in found if r["kind"] == "passes_path") + self.assertEqual( + search["search_dir"], + "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path", + ) + lists = {r["slot"]: r for r in found if r["kind"] == "names_searched"} + self.assertEqual(lists["dragon64"]["names"][:2], ["d64_1", "d64rom1"]) + self.assertEqual(lists["dragon64"]["names"][-1], "dragon") + self.assertEqual( + lists["d64_1"]["source_ref"], + "xroar/rd_assets/rd_config/xroar.conf:292, xroar/component_prepare.sh:13, 15", + ) + + def test_a_reference_to_an_unknown_romlist_raises(self): + with self.assertRaises(rd.LauncherPatternMissing): + launchers(self.files(XROAR_CONF.replace("@dragon", "@dragonx")), ["xroar"]) + + def test_a_preset_without_the_placeholder_raises(self): + with self.assertRaises(rd.LauncherPatternMissing): + launchers(self.files(XROAR_CONF.replace("RETRODECKBIOSDIR", "/bios")), ["xroar"]) + + +class UnknownConstructs(unittest.TestCase): + def test_an_unclassified_use_of_bios_path_raises(self): + broken = MELONDS_PREPARE.replace( + 'create_dir "$cheats_path/MelonDS"', 'rsync -a "$bios_path/" "$XDG_DATA_HOME/melonDS/"' + ) + files = melonds(broken) + with self.assertRaises(rd.LauncherPatternMissing): + launchers(files, ["melonds"]) + + def test_no_record_at_all_raises(self): + with self.assertRaises(rd.LauncherPatternMissing): + launchers({}, ["azahar"]) + + +class PlatformRecords(unittest.TestCase): + """The launchers platforms/retrodeck.yml carries, as the scraper wrote them.""" + + @classmethod + def setUpClass(cls) -> None: + cls.config = yaml.safe_load((REPO_ROOT / "platforms" / "retrodeck.yml").read_text()) + + def test_every_emulator_is_a_component_retrodeck_ships(self): + named = {r["emulator"] for r in self.config["launchers"]} + self.assertEqual(sorted(named - set(self.config["cores"])), []) + + def test_every_reference_is_in_a_component(self): + for record in self.config["launchers"]: + first = record["source_ref"].split("/", 1)[0] + self.assertIn(first, self.config["cores"], record) + + +if __name__ == "__main__": + unittest.main()