Files
libretro/tests/test_launchers_recalbox.py
T
Abdessamad Derraz fe9a081cfb feat: read Recalbox launchers from configgen
The scraper now records, from configgen at the pinned tag, the names each Recalbox launcher tries and where it hands them: the Hatari TOS cascades (standalone, hatari and hatarib cores), GSplus, Amiberry kickstarts, the PCSX2 region choice, the Dolphin IPL presence test, xemu, the mupen64plus 64DD IPL, the Mini vMac boot disk and the o2em_bios option. The system_directory libretroRetroarch gives each core, and the per-system overrides after it, are passes_path records.
2026-10-20 20:59:51 +02:00

1569 lines
69 KiB
Python

#!/usr/bin/env python3
"""Recalbox's launcher layer, read from configgen with ast.
The blocks below are copied from Recalbox 10.1.1 (projects/configgen) and
placed at the lines they hold there, so the source_ref an extraction writes
is checked against the tag itself.
"""
from __future__ import annotations
import json
import sys
import unittest
from collections.abc import Callable
from pathlib import Path
from unittest import mock
import yaml
REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT / "scripts"))
from scraper import recalbox_scraper # noqa: E402
from scraper.recalbox_launchers import ( # noqa: E402
ALL_CORES,
AMIBERRY_CONFIG,
AMIBERRY_KICKSTARTS,
AMIBERRY_SUBSYSTEMS,
CONFIGGEN,
CORES,
DOLPHIN,
FILES_MODULE,
GSPLUS,
HATARI,
LAUNCHER,
LIBRETRO_GENERATOR,
MUPEN_CONFIG,
PCSX2,
RETROARCH,
SOURCE_FILES,
XEMU,
Configgen,
Launcher,
LauncherShapeError,
amiberry,
content_files,
core_assets,
core_file_options,
dolphin,
extract_launchers,
gsplus,
hatari,
hatari_libretro,
mupen64plus,
pcsx2,
system_directories,
xemu,
)
from tests.test_launchers import placed # noqa: E402
# recalboxFiles.py:1-12, 173-174
FILES_MODULE_BLOCKS = {
1: """#!/usr/bin/env python
HOME_INIT = '/recalbox/share_init/system/'
HOME = '/recalbox/share/system'
CONF = HOME + '/configs'
SAVES = '/recalbox/share/saves'
SAVES_INIT = '/recalbox/share_init/saves'
SCREENSHOTS = '/recalbox/share/screenshots'
BIOS = '/recalbox/share/bios'
BIOS_INIT = '/recalbox/share_init/bios'
OVERLAYS = '/recalbox/share/overlays'
RECALBOX_OVERLAYS = '/recalbox/share_init/overlays'
RECALBOX_240P_OVERLAYS = '/recalbox/share_init/240poverlays'""",
173: """minivmacRomFile = '/tmp/minivmac.cmd'
minivmacOsFile = BIOS + '/macintosh/MinivMacBootv2.dsk'""",
}
# emulatorlauncher.py:10-19, 29-34, 47-52, 65-67, 80-82, 125-127, 131-136
LAUNCHER_BLOCKS = {
10: '''def getGenerator(emulator):
# We use a if...elif...else structure in order to instantiate the strict minimum required for an emulator
# and minimize static imports that are very time consuming, specially on low-end boards
if emulator == "advancemame":
module = __import__("configgen.generators.advancemame.advMameGenerator", fromlist=["AdvMameGenerator"])
generatorClass = getattr(module, "AdvMameGenerator")
elif emulator == "amiberry":
module = __import__("configgen.generators.amiberry.amiberryGenerator", fromlist=["AmiberryGenerator"])
generatorClass = getattr(module, "AmiberryGenerator")''',
29: ''' elif emulator == "dolphin":
module = __import__("configgen.generators.dolphin.dolphinGenerator", fromlist=["DolphinGenerator"])
generatorClass = getattr(module, "DolphinGenerator")
elif emulator == "dolphin-gui":
module = __import__("configgen.generators.dolphin.dolphinGenerator", fromlist=["DolphinGenerator"])
generatorClass = getattr(module, "DolphinGenerator")''',
47: ''' elif emulator == "gsplus":
module = __import__("configgen.generators.gsplus.gsplusGenerator", fromlist=["GSplusGenerator"])
generatorClass = getattr(module, "GSplusGenerator")
elif emulator == "hatari":
module = __import__("configgen.generators.hatari.hatariGenerator", fromlist=["HatariGenerator"])
generatorClass = getattr(module, "HatariGenerator")''',
65: ''' elif emulator == "mupen64plus":
module = __import__("configgen.generators.mupen.mupenGenerator", fromlist=["MupenGenerator"])
generatorClass = getattr(module, "MupenGenerator")''',
80: ''' elif emulator == "pcsx2":
module = __import__("configgen.generators.pcsx2.pcsx2Generator", fromlist=["Pcsx2Generator"])
generatorClass = getattr(module, "Pcsx2Generator")''',
125: ''' elif emulator == "xemu":
module = __import__("configgen.generators.xemu.xemuGenerator", fromlist=["XemuGenerator"])
generatorClass = getattr(module, "XemuGenerator")''',
131: ''' else:
print("Missing generator for {}".format(emulator))
raise ValueError
pass
return generatorClass()''',
}
# libretroRetroarch.py:1-11, 251-253, 421-529
RETROARCH_BLOCKS = {
1: '''from typing import List
import configgen.recalboxFiles as recalboxFiles
from configgen.Emulator import Emulator
from configgen.controllers.controller import ControllerPerPlayer
from configgen.settings.keyValueSettings import keyValueSettings
from configgen.generators.libretro.libretroChallengeLock import ChallengeLock
from configgen.utils.architecture import Architecture
class LibretroRetroarch:''',
251: ''' # Fill retroarch configuration
def fillRetroarchConfiguration(self):
settings: keyValueSettings = self.settings''',
421: '''
# Core assets path for dolphin only
if self.system.Core == "dolphin":
settings.setString("core_assets_directory", "/recalbox/share/bios/dolphin-emu/")
else:
settings.removeOption("core_assets_directory")
# Audio buffer (naomi2 anti-crackle, HDMI + CRT)
self.manage_audio_buffer(settings)
# Use bios in directories
# Most of the case are by core name
match self.system.Core:
case 'a5200':
settings.setString("system_directory", recalboxFiles.BIOS + "/atari5200")
case 'bsnes':
if self.system.Name == 'gb' or self.system.Name == 'gbc':
settings.setString("system_directory", recalboxFiles.BIOS + "/sgb")
elif self.system.Name == 'satellaview':
settings.setString("system_directory", recalboxFiles.BIOS + "/satellaview")
else:
settings.setString("system_directory", recalboxFiles.BIOS)
case 'bsneshd':
if self.system.Name == 'gb' or self.system.Name == 'gbc':
settings.setString("system_directory", recalboxFiles.BIOS + "/sgb")
elif self.system.Name == 'satellaview':
settings.setString("system_directory", recalboxFiles.BIOS + "/satellaview")
else:
settings.setString("system_directory", recalboxFiles.BIOS)
case 'cdi2015':
settings.setString("system_directory", recalboxFiles.BIOS + "/cdi")
case 'dolphin':
settings.setString("system_directory", recalboxFiles.BIOS + "/gamecube")
case 'emuscv':
settings.setString("system_directory", recalboxFiles.BIOS + "/scv")
case 'fmsx':
settings.setString("system_directory", recalboxFiles.BIOS + "/msx")
case 'freechaf':
settings.setString("system_directory", recalboxFiles.BIOS + "/channelf")
case 'freeintv':
settings.setString("system_directory", recalboxFiles.BIOS + "/intellivision")
case 'gambatte':
settings.setString("system_directory", recalboxFiles.BIOS + "/" + self.system.Name)
case 'gearsystem':
settings.setString("system_directory", recalboxFiles.BIOS + "/" + self.system.Name)
case 'genesisplusgx':
settings.setString("system_directory", recalboxFiles.BIOS + "/" + self.system.Name)
case 'genesisplusgxwide':
settings.setString("system_directory", recalboxFiles.BIOS + "/" + self.system.Name)
case 'geolith':
settings.setString("system_directory", recalboxFiles.BIOS + "/geolith")
case 'gpsp':
settings.setString("system_directory", recalboxFiles.BIOS + "/gba")
case 'mednafen_pce_fast':
settings.setString("system_directory", recalboxFiles.BIOS + "/" + self.system.Name)
case 'mednafen_pcfx':
settings.setString("system_directory", recalboxFiles.BIOS + "/pcfx")
case 'mednafen_supergrafx':
settings.setString("system_directory", recalboxFiles.BIOS + "/" + self.system.Name)
case 'geargrafx':
settings.setString("system_directory", recalboxFiles.BIOS + "/pcenginecd")
case 'mesen_s':
if self.system.SuperGameBoy:
settings.setString("system_directory", recalboxFiles.BIOS + "/sgb")
else:
settings.setString("system_directory", recalboxFiles.BIOS + "/" + self.system.Name)
case 'minivmac':
settings.setString("system_directory", recalboxFiles.BIOS + "/macintosh")
case 'mu':
settings.setString("system_directory", recalboxFiles.BIOS + "/palm")
case 'o2em':
settings.setString("system_directory", recalboxFiles.BIOS + "/o2em")
case 'opera':
settings.setString("system_directory", recalboxFiles.BIOS + "/3do")
case 'picodrive':
settings.setString("system_directory", recalboxFiles.BIOS + "/" + self.system.Name)
case 'pokemini':
settings.setString("system_directory", recalboxFiles.BIOS + "/pokemini")
case 'prosystem':
settings.setString("system_directory", recalboxFiles.BIOS + "/atari7800")
case 'sameboy':
if self.system.SuperGameBoy:
settings.setString("system_directory", recalboxFiles.BIOS + "/sgb")
else:
settings.setString("system_directory", recalboxFiles.BIOS + "/" + self.system.Name)
case 'snes9x':
if self.system.Name == 'satellaview':
settings.setString("system_directory", recalboxFiles.BIOS + "/satellaview")
elif self.system.Name == 'sufami':
settings.setString("system_directory", recalboxFiles.BIOS + "/sufami")
else:
settings.setString("system_directory", recalboxFiles.BIOS)
case _:
settings.setString("system_directory", recalboxFiles.BIOS)
# We have a few to use by system name - when there are multiple cores with same bios
amigaSystems = ["amiga600", "amiga1200", "amigacdtv", "amigacd32"]
if self.system.Name in amigaSystems:
settings.setString("system_directory", recalboxFiles.BIOS + "/amiga/bios")
elif self.system.Name == 'fds':
settings.setString("system_directory", recalboxFiles.BIOS + "/fds")
elif self.system.Name == 'lynx':
settings.setString("system_directory", recalboxFiles.BIOS + "/lynx")
elif self.system.Name == 'nds':
settings.setString("system_directory", recalboxFiles.BIOS + "/nds")
elif self.system.Name == 'saturn':
settings.setString("system_directory", recalboxFiles.BIOS + "/saturn")
elif self.system.Name == 'stv':
settings.setString("system_directory", recalboxFiles.BIOS + "/stv")''',
}
# libretroCores.py:1-17, 165-298, 348-355, 492-504, 633-705
CORES_BLOCKS = {
1: '''from typing import Dict
from configgen.Emulator import Emulator
from configgen.crt.CRTTypes import CRTAdapter
from configgen.settings.keyValueSettings import keyValueSettings
from configgen.controllers.controller import ControllerPerPlayer
from configgen.utils.architecture import Architecture
def _jvsCabFamily() -> str:
try:
with open("/sys/devices/platform/recalboxjvs/cab_family") as f:
return f.read().strip()
except OSError:
return ""
class LibretroCores:''',
165: r''' def configureAtariSTNew(self, coreSettings: keyValueSettings):
# Machine list as defined in HatariB core
MACHINE_UNKNOWN = -1
MACHINE_ST = 0
MACHINE_MEGA_ST = 1
MACHINE_STE = 2
MACHINE_MEGA_STE = 3
MACHINE_TT = 4
MACHINE_FALCON = 5
# Try identify machine folder
machines: dict[str, int] = \
{
"atari-st": MACHINE_ST,
"/st/": MACHINE_ST,
"atarimegast": MACHINE_MEGA_ST,
"atari-megast": MACHINE_MEGA_ST,
"atari-mega-st": MACHINE_MEGA_ST,
"megast": MACHINE_MEGA_ST,
"mega-st": MACHINE_MEGA_ST,
"atariste": MACHINE_STE,
"atari-ste": MACHINE_STE,
"/ste/": MACHINE_STE,
"atarimegaste": MACHINE_MEGA_STE,
"atari-megaste": MACHINE_MEGA_STE,
"atari-mega-ste": MACHINE_MEGA_STE,
"megaste": MACHINE_MEGA_STE,
"mega-ste": MACHINE_MEGA_STE,
"ataritt": MACHINE_TT,
"atari-tt": MACHINE_TT,
"/tt/": MACHINE_TT,
"falcon": MACHINE_FALCON,
"atari-falcon": MACHINE_FALCON,
}
defaultMachine: int = MACHINE_UNKNOWN
romLower: str = self.rom.lower()
for key in machines:
if key in romLower:
defaultMachine: int = machines[key]
break
if not defaultMachine == MACHINE_UNKNOWN:
coreSettings.setInt("hatarib_machine", defaultMachine)
print("MACHINE: {}".format(defaultMachine))
else:
defaultMachine = coreSettings.getInt("hatarib_machine", MACHINE_ST)
# Select bios
subdir: str = "atarist"
import os
biosFile = os.path.join(subdir, "st.img")
# ST/MEGAST use both st.img
if defaultMachine == MACHINE_FALCON : biosFile = os.path.join(subdir, "falcon.img")
elif defaultMachine == MACHINE_MEGA_STE: biosFile = os.path.join(subdir, "megaste.img")
elif defaultMachine == MACHINE_TT : biosFile = os.path.join(subdir, "tt.img")
elif defaultMachine == MACHINE_STE : biosFile = os.path.join(subdir, "ste.img")
import configgen.recalboxFiles as recalboxFiles
if not os.path.exists(os.path.join(recalboxFiles.BIOS, biosFile)):
biosFile = os.path.join(subdir, "tos.img")
if not os.path.exists(os.path.join(recalboxFiles.BIOS, biosFile)):
biosFile = "tos.img"
if not os.path.exists(os.path.join(recalboxFiles.BIOS, biosFile)):
biosFile = "<etos1024k>"
if "emutos" in self.rom:
biosFile = os.path.join(recalboxFiles.BIOS, subdir, "<etos1024k>")
coreSettings.setString("hatarib_tos", str(biosFile))
print("BIOS : {}".format(biosFile))
def configureAtariST(self, _):
# Machines as defined in old Hatari core
MACHINE_UNKNOWN = -1
MACHINE_ST = 0
MACHINE_STE = 1
MACHINE_TT = 2
MACHINE_FALCON = 3,
MACHINE_MEGA_STE = 4
# Load config
from configgen.settings.iniSettings import IniSettings
import configgen.recalboxFiles as recalboxFiles
atariStSettings = IniSettings(recalboxFiles.hatariCustomConfig)
atariStSettings.loadFile(True)
# Auto set machine
from typing import Dict
# Try identify machine folder
machines: Dict[str, int] = \
{
"atari-st": MACHINE_ST,
"/st/": MACHINE_ST,
"atariste": MACHINE_STE,
"atari-ste": MACHINE_STE,
"/ste/": MACHINE_STE,
"ataritt": MACHINE_TT,
"atari-tt": MACHINE_TT,
"/tt/": MACHINE_TT,
"atarimegaste": MACHINE_MEGA_STE,
"atari-megaste": MACHINE_MEGA_STE,
"atari-mega-ste": MACHINE_MEGA_STE,
"megaste": MACHINE_MEGA_STE,
"mega-ste": MACHINE_MEGA_STE,
"falcon": MACHINE_FALCON,
"atari-falcon": MACHINE_FALCON,
}
defaultMachine: int = MACHINE_UNKNOWN
romLower: str = self.rom.lower()
for key in machines:
if key in romLower:
defaultMachine: int = machines[key]
break
if not defaultMachine == MACHINE_UNKNOWN:
print("MACHINE: {}".format(defaultMachine))
atariStSettings.setString("System", "nMachineType", str(defaultMachine))
# Select bios
import os
subdir: str = "atarist"
biosPath = os.path.join(recalboxFiles.BIOS, subdir, "st.img")
if defaultMachine == MACHINE_STE : biosPath = os.path.join(recalboxFiles.BIOS, subdir, "ste.img")
elif defaultMachine == MACHINE_TT : biosPath = os.path.join(recalboxFiles.BIOS, subdir, "tt.img")
elif defaultMachine == MACHINE_MEGA_STE: biosPath = os.path.join(recalboxFiles.BIOS, subdir, "megaste.img")
elif defaultMachine == MACHINE_FALCON : biosPath = os.path.join(recalboxFiles.BIOS, subdir, "falcon.img")
if not os.path.exists(biosPath):
biosPath = os.path.join(recalboxFiles.BIOS, subdir, "tos.img")
if not os.path.exists(biosPath):
biosPath = os.path.join(recalboxFiles.BIOS, "tos.img")
if not os.path.exists(biosPath):
biosPath = os.path.join(recalboxFiles.BIOS, subdir, "emutos.img")
if "emutos" in self.rom:
biosPath = os.path.join(recalboxFiles.BIOS, subdir, "emutos.img")
atariStSettings.setString("ROM", "szTosImageFileName", str(biosPath))
print("BIOS : {}".format(biosPath))
# Save config
atariStSettings.saveFile()''',
348: '''
@staticmethod
def configureOdyssey2(coreSettings: keyValueSettings):
coreSettings.setString("o2em_bios", '"o2rom.bin"')
@staticmethod
def configureVideoPacPlus(coreSettings: keyValueSettings):
coreSettings.setString("o2em_bios", '"g7400.bin"')''',
492: ''' def configureDolphin(self, coreSettings: keyValueSettings) -> None:
coreSettings.setString("dolphin_emulator_speed", '"1.0"')
coreSettings.setString("dolphin_cpu_clock_rate", '"1.0"')
ipl0 = "/recalbox/share/bios/gamecube/EUR/IPL.bin"
ipl1 = "/recalbox/share/bios/gamecube/JP/IPL.bin"
ipl2 = "/recalbox/share/bios/gamecube/USA/IPL.bin"
import os.path
if os.path.exists(ipl0) or os.path.exists(ipl1) or os.path.exists(ipl2):
coreSettings.setString("dolphin_skip_gc_bios", '"disabled"')
else:
coreSettings.setString("dolphin_skip_gc_bios", '"enabled"')''',
633: r''' # Fill cores configuration
def fillCoresConfiguration(self):
settings = self.settings
# Specific configuration handlers per core
specificCoreHandlers =\
{
"bluemsx" : LibretroCores.configureBlueMSX,
"dolphin": self.configureDolphin,
"mame2003_plus": LibretroCores.configureMAME2003plus,
"mame": self.configureMAME,
"mame0258": self.configureMAME,
"mame0278": self.configureMAME,
"mupen64plus_nx" : LibretroCores.configureMUPEN64,
"opera": LibretroCores.configureOpera,
"parallel_n64": LibretroCores.configurePARALLELN64,
"np2kai" : LibretroCores.configureNPKAI,
"picodrive" : self.configurePicodrive,
"swanstation" : self.configureSwanstation,
"dosbox_pure" : LibretroCores.configureDosBoxPure,
"flycast": self.configureFlycast,
"flycast-next": self.configureFlycast,
"fbneo": self.configureFBNeo,
"scummvm": self.configureScummvm,
"hatari": self.configureAtariST,
"hatarib": self.configureAtariSTNew,
"melonds": LibretroCores.configureMelonDS,
"desmume": LibretroCores.configureDesmume,
"ppsspp": self.configurePPSSPP,
"pcsx_rearmed": self.configurePCSXRearmed,
"mednafen_psx_hw": self.configureBeetlePSX,
"mednafen_psx": self.configureBeetlePSX,
"gambatte": self.configureGambatte,
"mednafen_stv" : self.configureSTV,
"eightyone": self.configureEightyOne,
"tgbdual": self.configureTGBDual,
}
# Get handler and execute
core = self.system.Core
if core in specificCoreHandlers:
function = specificCoreHandlers.get(core)
function(settings)
# Specific configuration handlers per system
specificSystemHandlers =\
{
"amiga600" : LibretroCores.configureAmiga600,
"amiga1200" : LibretroCores.configureAmiga1200,
"amigacdtv" : LibretroCores.configureAmigaCDTV,
"amigacd32" : LibretroCores.configureAmigaCD32,
"atari5200" : LibretroCores.configureAtari5200,
"atari800" : LibretroCores.configureAtari800,
"amstradcpc" : LibretroCores.configureAmstradCPC,
"gx4000" : LibretroCores.configureAmstradGX4000,
"nds" : self.configureNDS,
"spectravideo" : LibretroCores.configureSpectravideo,
"64dd" : LibretroCores.configure64DD,
"msx1" : LibretroCores.configureMsx1,
"msx2" : LibretroCores.configureMsx2,
"msxturbor" : LibretroCores.configureMsxTurboR,
"pc98" : LibretroCores.configureNPKAI,
"odyssey2" : LibretroCores.configureOdyssey2,
"videopacplus" : LibretroCores.configureVideoPacPlus,
"sg1000" : LibretroCores.configureSg1000,
"naomi2" : self.configureNaomi2,
"saturn" : self.configureSaturn,
}
# Get handler and execute
if self.system.Name in specificSystemHandlers:
function = specificSystemHandlers.get(self.system.Name)
function(settings)''',
}
# libretroGenerator.py:7, 22, 814-815, 876-883
LIBRETRO_GENERATOR_BLOCKS = {
7: '''import configgen.recalboxFiles as recalboxFiles''',
22: '''class LibretroGenerator(Generator):''',
814: ''' @staticmethod
def buildRomArguments(rom: str, core: str, verbose: bool, demo: bool) -> List[str]:''',
876: ''' # macintosh
if core == "minivmac":
with open(recalboxFiles.minivmacRomFile, 'w') as f:
f.write('"Mini vMac" "' + recalboxFiles.minivmacOsFile + '" "' + rom + '"')
rom = [recalboxFiles.minivmacRomFile]
return rom
''',
}
# hatariGenerator.py:1-14, 103-167
HATARI_BLOCKS = {
1: '''import os
from typing import List, Dict
from configgen.Command import Command
from configgen.Emulator import Emulator
from configgen.controllers.controller import ControllerPerPlayer
from configgen.generators.Generator import Generator
import configgen.recalboxFiles as recalboxFiles
from configgen.settings.iniSettings import IniSettings
import configgen.settings.configOverriding as configOverriding
from configgen.settings.keyValueSettings import keyValueSettings
class HatariGenerator(Generator):''',
103: r'''
@staticmethod
def SetMachineAndBios(config: IniSettings, rom: str):
# Machine list
MACHINE_UNKNOWN = -1
MACHINE_ST = 0
MACHINE_MEGA_ST = 1
MACHINE_STE = 2
MACHINE_MEGA_STE = 3
MACHINE_TT = 4
MACHINE_FALCON = 5
# Try identify machine folder
machines: Dict[str, int] = \
{
"atari-st": MACHINE_ST,
"/st/": MACHINE_ST,
"atarimegast": MACHINE_MEGA_ST,
"atari-megast": MACHINE_MEGA_ST,
"atari-mega-st": MACHINE_MEGA_ST,
"megast": MACHINE_MEGA_ST,
"mega-st": MACHINE_MEGA_ST,
"atariste": MACHINE_STE,
"atari-ste": MACHINE_STE,
"/ste/": MACHINE_STE,
"atarimegaste": MACHINE_MEGA_STE,
"atari-megaste": MACHINE_MEGA_STE,
"atari-mega-ste": MACHINE_MEGA_STE,
"megaste": MACHINE_MEGA_STE,
"mega-ste": MACHINE_MEGA_STE,
"ataritt": MACHINE_TT,
"atari-tt": MACHINE_TT,
"/tt/": MACHINE_TT,
"falcon": MACHINE_FALCON,
"atari-falcon": MACHINE_FALCON,
}
defaultMachine: int = MACHINE_UNKNOWN
romLower: str = rom.lower()
for key in machines:
if key in romLower:
defaultMachine: int = machines[key]
break
if not defaultMachine == MACHINE_UNKNOWN:
config.setInt("System", "nMachineType", defaultMachine)
print("MACHINE: {}".format(defaultMachine))
else:
defaultMachine = coreSettings.getInt("hatarib_machine", MACHINE_ST)
# Select bios
subdir: str = "atarist"
biosFile = os.path.join(recalboxFiles.BIOS, subdir, "st.img")
# ST/MEGAST use both st.img
if defaultMachine == MACHINE_FALCON : biosFile = os.path.join(recalboxFiles.BIOS, subdir, "falcon.img")
elif defaultMachine == MACHINE_MEGA_STE: biosFile = os.path.join(recalboxFiles.BIOS, subdir, "megaste.img")
elif defaultMachine == MACHINE_TT : biosFile = os.path.join(recalboxFiles.BIOS, subdir, "tt.img")
elif defaultMachine == MACHINE_STE : biosFile = os.path.join(recalboxFiles.BIOS, subdir, "ste.img")
if not os.path.exists(biosFile):
biosFile = os.path.join(recalboxFiles.BIOS, subdir, "tos.img")
if not os.path.exists(biosFile):
biosFile = os.path.join(recalboxFiles.BIOS, "tos.img")
if not os.path.exists(biosFile):
biosFile = os.path.join(recalboxFiles.BIOS, subdir, "emutos.img")
if "emutos" in rom:
biosFile = os.path.join(recalboxFiles.BIOS, subdir, "emutos.img")
config.setString("ROM", "szTosImageFileName", str(biosFile))''',
}
# gsplusGenerator.py:1-11, 93-148
GSPLUS_BLOCKS = {
1: '''from typing import List
from configgen.Command import Command
import configgen.recalboxFiles as recalboxFiles
from configgen.Emulator import Emulator
from configgen.generators.Generator import Generator
from configgen.settings.keyValueSettings import keyValueSettings
from configgen.controllers.controller import Controller, InputItem, ControllerPerPlayer
class GSplusGenerator(Generator):''',
93: ''' def generate(self, system: Emulator, playersControllers: ControllerPerPlayer, recalboxOptions: keyValueSettings, args):
"""
Load, override keys and save back emulator's configuration file
This way, any modification is kept accross emulator launches
"""
slot: int = -1
if ".gsplus" in args.rom:
settings = keyValueSettings(args.rom, True)
settings.loadFile(True)
settings.changeSettingsFile(recalboxFiles.gsplusConfig)
# Adjust BIOS path
import os
if settings.getString("g_cfg_rom_path", "") == "%1":
settings.setString("g_cfg_rom_path", os.path.join(recalboxFiles.BIOS, "apple2gs1.rom"))
elif settings.getString("g_cfg_rom_path", "") == "%3":
settings.setString("g_cfg_rom_path", os.path.join(recalboxFiles.BIOS, "apple2gs3.rom"))
# Adjust rom path
folder = os.path.dirname(args.rom)
for i in range(1, 33):
if i < 3:
if settings.hasOption("s5d{}".format(i)) and settings.getString("s5d{}".format(i), "") != "":
settings.setString("s5d{}".format(i), os.path.join(folder, settings.getString("s5d{}".format(i), "")))
if settings.hasOption("s6d{}".format(i)) and settings.getString("s6d{}".format(i), "") != "":
settings.setString("s6d{}".format(i), os.path.join(folder, settings.getString("s6d{}".format(i), "")))
if settings.hasOption("s7d{}".format(i)) and settings.getString("s7d{}".format(i), "") != "":
settings.setString("s7d{}".format(i), os.path.join(folder, settings.getString("s7d{}".format(i), "")))
else:
# Load config file
settings = keyValueSettings(recalboxFiles.gsplusConfig, True)
settings.loadFile(True)
# Reset drive slots
for i in range(1, 33):
if i < 3:
if settings.hasOption("s5d{}".format(i)):
settings.setString("s5d{}".format(i), "")
if settings.hasOption("s6d{}".format(i)):
settings.setString("s6d{}".format(i), "")
if settings.hasOption("s7d{}".format(i)):
settings.setString("s7d{}".format(i), "")
# Reset bios
import os
bios = os.path.join(recalboxFiles.BIOS, "apple2gs1.rom")
if os.path.exists(bios):
settings.setString("g_cfg_rom_path", bios)
else:
bios = os.path.join(recalboxFiles.BIOS, "apple2gs3.rom")
if os.path.exists(bios):
settings.setString("g_cfg_rom_path", bios)
else:
settings.removeOption("g_cfg_rom_path")''',
}
# amiberryKickstarts.py:1-151
AMIBERRY_KICKSTARTS_BLOCKS = {
1: r'''#!/usr/bin/env python
import os
import configgen.recalboxFiles as recalboxFiles
from configgen.generators.amiberry.amiberryRomType import RomType
from configgen.generators.amiberry.amiberrySubSystems import SubSystems
# Kickstarts live in the amiga subfolder (see es_bios.xml and amiberry rom_path)
def AmigaBios(name: str) -> str:
return os.path.join(recalboxFiles.BIOS, "amiga", "bios", name)
class KickstartManager:
BIOS_LIST = \
{
# Kickstart v1.2 rev 33.180 (1986)(Commodore)(A500-A1000-A2000).rom
"kick33180.A500": (256, "KS ROM v1.2 (A500,A1000,A2000) rev 33.180 (256k) [315093-01]"),
# Kickstart v1.3 rev 34.5 (1987)(Commodore)(A500-A1000-A2000-CDTV).rom
"kick34005.A500": (256, "KS ROM v1.3 (A500,A1000,A2000) rev 34.5 (256k) [315093-02]"),
# Kickstart v2.04 rev 37.175 (1991)(Commodore)(A500+).rom
"kick37175.A500": (256, "KS ROM v2.04 (A500+) rev 37.175 (512k) [390979-01]"),
# Kickstart v3.0 rev 39.106 (1992)(Commodore)(A1200)[!].rom
"kick39106.A1200": (256, "KS ROM v3.0 (A1200) rev 39.106 (512k) [391523-01/391524-01]"),
# Kickstart v3.1 rev 40.63 (1993)(Commodore)(A500-A600-A2000)[!].rom
"kick40063.A600": (256, "KS ROM v3.1 (A600) rev 40.63 (512k)"),
# Kickstart v3.1 rev 40.68 (1993)(Commodore)(A1200).rom
"kick40068.A1200": (256, "KS ROM v3.1 (A1200) rev 40.68 (512k) [391773-01/391774-01]"),
# Kickstart v3.1 rev 40.68 (1993)(Commodore)(A4000).rom
"kick40068.A4000": (256, "KS ROM v3.1 (A4000) rev 40.68 (512k)"),
# Kickstart v1.3 rev 34.5 (1987)(Commodore)(A500-A2000-CDTV)[!].rom
"kick34005.CDTV": (256, "CDTV Extended ROM rev 34.5 (256k)"),
# Kickstart v3.1 rev 40.60 (1993)(Commodore)(CD32).rom
"kick40060.CD32": (512, "CD32 KS ROM v3.1 rev 40.60 (512k)"),
# CD32 Extended-ROM rev 40.60 (1993)(Commodore)(CD32).rom 87746be2
"kick40060.CD32.ext": (1024, "CD32 extended ROM rev 40.60 (512k)"),
# AROS Kickstart 2019
"kick02019.AROS": (256, "KS AROS 2019 Part#1 (512k)"),
# AROS Extended Kickstart 2019
"kick02019.AROS.ext": (256, "KS AROS 2019 Part#2 (512k)"),
}
BIOS_PER_SYSTEM = \
{
SubSystems.A600:
[
( # Use the highest kickstart with advanced hardware
[RomType.WHDL, RomType.HDF, RomType.PACKAGE, RomType.CDROM, RomType.HDDFS],
["kick37175.A500", "kick34005.A500", "kick33180.A500"]
),
( # Use kickstarts 1.3/1.2 to maximize compatibility when using disks
[RomType.DISK],
["kick34005.A500", "kick33180.A500"]
),
],
SubSystems.A1200:
[
( # Use kickstart 3.1 first, then 3.0. Fallback on A4000 kickstart
[RomType.WHDL, RomType.HDF, RomType.PACKAGE, RomType.CDROM, RomType.HDDFS, RomType.DISK],
["kick40068.A1200", "kick39106.A1200", "kick40068.A4000"]
),
],
SubSystems.CD32:
[
( # No choice for the CD32
[RomType.CDROM],
["kick40060.CD32|kick40060.CD32.ext"]
),
],
SubSystems.CDTV:
[
( # No choice for the CD32
[RomType.CDROM],
["kick34005.A500|kick34005.CDTV"]
),
],
}
def __init__(self):
self.rom: str = ""
self.ext: str = ""
@staticmethod
def GenerateWHDSymLinks(destinationfolder: str):
for bios in KickstartManager.BIOS_LIST:
biosPath = AmigaBios(bios)
biosLink = os.path.join(destinationfolder, bios)
if not os.path.exists(biosLink) and os.path.exists(biosPath):
os.symlink(biosPath, biosLink)
def GetKickstartsFor(self, subsystem: SubSystems, romtype: RomType):
# Select best available kickstart
biosFile = None
extFile = None
needExt = False
tuppleArray = KickstartManager.BIOS_PER_SYSTEM[subsystem]
for tupple in tuppleArray:
romTypes, biosList = tupple
if romtype in romTypes:
for biosCouple in biosList:
bios = biosCouple
ext = None
if '|' in biosCouple:
bios, ext = biosCouple.split('|', 2)
needExt = ext is not None
biosPath = AmigaBios(bios)
if os.path.exists(biosPath):
biosFile = biosPath
if needExt:
extPath = AmigaBios(ext)
if os.path.exists(extPath):
extFile = extPath
if biosFile is not None:
break
# Fallbacks
if subsystem in SubSystems.COMPUTERS:
# Fallback to AROS 2019?
if biosFile is None:
arosBios = AmigaBios("kick02019.AROS")
arosExt = AmigaBios("kick02019.AROS.ext")
if os.path.exists(arosBios) and os.path.exists(arosExt):
biosFile = arosBios
extFile = arosExt
# Fallback to internal Aros bios
if biosFile is None:
self.rom = ":AROS"
return
# Last check
if biosFile is None:
raise Exception("No BIOS found")
# If an extened bios is required, it's not worth going further w/o it
if extFile is None and needExt:
raise Exception("Extended BIOS not found")
self.rom = biosFile if biosFile is not None else ""
self.ext = extFile if extFile is not None else ""
@property
def GetBIOS(self) -> str: return self.rom
@property
def NeedExtendedBIOS(self) -> bool: return len(self.ext) != 0
@property
def GetExtendedBIOS(self) -> str: return self.ext''',
}
# amiberrySubSystems.py:1-13
AMIBERRY_SUBSYSTEMS_BLOCKS = {
1: '''#!/usr/bin/env python
class SubSystems:
UNKNOWN = "unknown"
A600 = "amiga600"
A1200 = "amiga1200"
CDTV = "amigacdtv"
CD32 = "amigacd32"
COMPUTERS = ( A600, A1200 )
CONSOLES = ( CDTV, CD32 )
HAVEMOUSE = ( A600, A1200, CDTV, CD32 )''',
}
# amiberryConfig.py:1-9, 337-345
AMIBERRY_CONFIG_BLOCKS = {
1: '''#!/usr/bin/env python
from typing import List
from configgen.generators.amiberry.amiberrySubSystems import SubSystems
from configgen.controllers.controller import ControllerPerPlayer
from configgen.generators.amiberry.amiberryRomType import RomType
class ConfigGenerator:''',
337: ''' def SetKickstarts(self, subsystem: SubSystems, romtype: RomType):
from configgen.generators.amiberry.amiberryKickstarts import KickstartManager
manager = KickstartManager()
manager.GetKickstartsFor(subsystem, romtype)
kickstart = manager.GetBIOS
self.settings.setString("kickstart_rom_file", kickstart)
if manager.NeedExtendedBIOS:
extended = manager.GetExtendedBIOS
self.settings.setString("kickstart_ext_rom_file", extended)''',
}
# pcsx2Generator.py:1-166
PCSX2_BLOCKS = {
1: r'''#!/usr/bin/env python
from typing import Dict
from configgen.Command import Command
from configgen.Emulator import Emulator
from configgen.controllers.controller import ControllerPerPlayer
from configgen.emulatorlauncher import recalboxFiles
from configgen.generators.Generator import Generator
from configgen.settings.iniSettings import IniSettings
from configgen.settings.keyValueSettings import keyValueSettings
class Pcsx2Generator(Generator):
pcsx2RootFolder = recalboxFiles.HOME + '/.config/PCSX2'
pcsx2ConfigFileReg = pcsx2RootFolder + '/PCSX2-reg.ini'
pcsx2ConfigFileGSdx = pcsx2RootFolder + '/GSdx.ini'
pcsx2ConfigFilePAD = pcsx2RootFolder + '/PAD.ini'
pcsx2ConfigFilePCSX2_ui = pcsx2RootFolder + '/PCSX2_ui.ini'
pcsx2ConfigFilePCSX2_vm = pcsx2RootFolder + '/PCSX2_vm.ini'
pcsx2ConfigFileSPU2 = pcsx2RootFolder + '/SPU2.ini'
SECTION_DEFAULT = "default"
# Core
SECTION_VM_EMUCORE_GS = "EmuCore/GS"
SECTION_VM_EMUCORE_SPEEDHACKS = "EmuCore/Speedhacks"
# UI
SECTION_UI_NONE = ""
SECTION_UI_GSWINDOWS = "GSWindow"
SECTION_UI_POPUPDISABLERS = "PopupDisablers"
SECTION_UI_FILENAMES = "Filenames"
# Sound (SPU2.ini)
SECTION_SPU2_MIXING = "MIXING"
SECTION_SPU2_OUTPUT = "OUTPUT"
SECTION_SPU2_SDL = "SDL"
# Bios names
BIOS_USA = "ps2-0230a-20080220.bin"
BIOS_EU = "ps2-0230e-20080220.bin"
BIOS_JP = "ps2-0230j-20080220.bin"
BIOS_HK = "ps2-0230h-20080220.bin"
@staticmethod
def __ConfigurePad(playersControllers: ControllerPerPlayer, system: Emulator):
with open(Pcsx2Generator.pcsx2ConfigFilePAD, "w") as f:
if system.Rumble:
f.write("options = 1\n")
f.write("mouse_sensitivity = 100\n")
f.write("ff_intensity = 32767\n")
if 1 in playersControllers: f.write("uid[0] = {}\n".format(playersControllers[1].SdlIndex))
if 2 in playersControllers: f.write("uid[1] = {}\n".format(playersControllers[2].SdlIndex))
if 1 in playersControllers: f.write("SDL2 = {}\n".format(playersControllers[1].generateSDLGameDBLine(False)))
if 2 in playersControllers: f.write("SDL2 = {}\n".format(playersControllers[2].generateSDLGameDBLine(False)))
@staticmethod
def __CheckPs2Bios(rom: str) -> str:
import os.path
def Exists(romPath: str) -> bool:
return os.path.exists(os.path.join(recalboxFiles.BIOS, "ps2", romPath))
# Try to get Redump region
lrom = rom.lower()
# EU region
if "europe" in lrom and Exists(Pcsx2Generator.BIOS_EU): return Pcsx2Generator.BIOS_EU
elif "france" in lrom and Exists(Pcsx2Generator.BIOS_EU): return Pcsx2Generator.BIOS_EU
elif "ireland" in lrom and Exists(Pcsx2Generator.BIOS_EU): return Pcsx2Generator.BIOS_EU
elif "germany" in lrom and Exists(Pcsx2Generator.BIOS_EU): return Pcsx2Generator.BIOS_EU
elif "belgium" in lrom and Exists(Pcsx2Generator.BIOS_EU): return Pcsx2Generator.BIOS_EU
elif "italy" in lrom and Exists(Pcsx2Generator.BIOS_EU): return Pcsx2Generator.BIOS_EU
elif "greece" in lrom and Exists(Pcsx2Generator.BIOS_EU): return Pcsx2Generator.BIOS_EU
elif "croatia" in lrom and Exists(Pcsx2Generator.BIOS_EU): return Pcsx2Generator.BIOS_EU
elif "denmark" in lrom and Exists(Pcsx2Generator.BIOS_EU): return Pcsx2Generator.BIOS_EU
elif "spain" in lrom and Exists(Pcsx2Generator.BIOS_EU): return Pcsx2Generator.BIOS_EU
elif "portugal" in lrom and Exists(Pcsx2Generator.BIOS_EU): return Pcsx2Generator.BIOS_EU
elif "poland" in lrom and Exists(Pcsx2Generator.BIOS_EU): return Pcsx2Generator.BIOS_EU
elif "finland" in lrom and Exists(Pcsx2Generator.BIOS_EU): return Pcsx2Generator.BIOS_EU
elif "russia" in lrom and Exists(Pcsx2Generator.BIOS_EU): return Pcsx2Generator.BIOS_EU
elif "austria" in lrom and Exists(Pcsx2Generator.BIOS_EU): return Pcsx2Generator.BIOS_EU
elif "netherlands" in lrom and Exists(Pcsx2Generator.BIOS_EU): return Pcsx2Generator.BIOS_EU
elif "switzerland" in lrom and Exists(Pcsx2Generator.BIOS_EU): return Pcsx2Generator.BIOS_EU
elif "norway" in lrom and Exists(Pcsx2Generator.BIOS_EU): return Pcsx2Generator.BIOS_EU
elif "sweden" in lrom and Exists(Pcsx2Generator.BIOS_EU): return Pcsx2Generator.BIOS_EU
elif "scandinavia" in lrom and Exists(Pcsx2Generator.BIOS_EU): return Pcsx2Generator.BIOS_EU
elif "australia" in lrom and Exists(Pcsx2Generator.BIOS_EU): return Pcsx2Generator.BIOS_EU
elif "india" in lrom and Exists(Pcsx2Generator.BIOS_EU): return Pcsx2Generator.BIOS_EU
elif "south africa" in lrom and Exists(Pcsx2Generator.BIOS_EU): return Pcsx2Generator.BIOS_EU
# USA region
elif "canada" in lrom and Exists(Pcsx2Generator.BIOS_USA): return Pcsx2Generator.BIOS_USA
elif "brazil" in lrom and Exists(Pcsx2Generator.BIOS_USA): return Pcsx2Generator.BIOS_USA
elif "latin america" in lrom and Exists(Pcsx2Generator.BIOS_USA): return Pcsx2Generator.BIOS_USA
# JP region
elif "japan" in lrom and Exists(Pcsx2Generator.BIOS_JP): return Pcsx2Generator.BIOS_JP
elif "korea" in lrom and Exists(Pcsx2Generator.BIOS_JP): return Pcsx2Generator.BIOS_JP
elif "asia" in lrom and Exists(Pcsx2Generator.BIOS_JP): return Pcsx2Generator.BIOS_JP
# CH
elif "china" in lrom and Exists(Pcsx2Generator.BIOS_JP): return Pcsx2Generator.BIOS_HK
elif "taiwan" in lrom and Exists(Pcsx2Generator.BIOS_JP): return Pcsx2Generator.BIOS_HK
# Shortest name at the end
elif "uk" in lrom and Exists(Pcsx2Generator.BIOS_EU): return Pcsx2Generator.BIOS_EU
elif "usa" in lrom and Exists(Pcsx2Generator.BIOS_USA): return Pcsx2Generator.BIOS_USA
# Check if PS2 bios bin in folders
for bios in (Pcsx2Generator.BIOS_USA, Pcsx2Generator.BIOS_EU, Pcsx2Generator.BIOS_JP, Pcsx2Generator.BIOS_HK):
if Exists(bios):
return bios
return ""
@staticmethod
def __ConfigureReg():
reg = keyValueSettings(Pcsx2Generator.pcsx2ConfigFileReg)
reg.loadFile(True)
# Override Reg Configuration
reg.setString("DocumentsFolderMode", "User")
reg.setString("CustomDocumentsFolder", "/usr/bin/PCSX2")
reg.setString("UseDefaultSettingsFolder", "enabled")
reg.setString("SettingsFolder", "/recalbox/share/system/.config/PCSX2")
reg.setString("Install_Dir", "/usr/bin/PCSX2")
reg.setString("RunWizard", "0")
# Save configuration
reg.saveFile()
@staticmethod
def __ConfigureCore():
import multiprocessing
NbCpu = multiprocessing.cpu_count()
# Override VM Configuration
vm = IniSettings(Pcsx2Generator.pcsx2ConfigFilePCSX2_vm, True)
vm.loadFile(True)
vm.setString(Pcsx2Generator.SECTION_VM_EMUCORE_GS, "FrameLimitEnable", "enabled")
vm.setString(Pcsx2Generator.SECTION_VM_EMUCORE_GS, "FramerateNTSC", "59.94")
vm.setInt(Pcsx2Generator.SECTION_VM_EMUCORE_GS, "FrameratePAL", 50)
vm.setString(Pcsx2Generator.SECTION_VM_EMUCORE_GS, "FrameSkipEnable", "disabled")
vm.setInt(Pcsx2Generator.SECTION_VM_EMUCORE_GS, "FramesToDraw", 2)
vm.setInt(Pcsx2Generator.SECTION_VM_EMUCORE_GS, "FramesToSkip", 2)
vm.setString(Pcsx2Generator.SECTION_VM_EMUCORE_GS, "LimitScalar", "1.00")
vm.setString(Pcsx2Generator.SECTION_VM_EMUCORE_GS, "SynchronousMTGS", "disabled")
vm.setInt(Pcsx2Generator.SECTION_VM_EMUCORE_GS, "VsyncQueueSize", 2)
vm.setString(Pcsx2Generator.SECTION_VM_EMUCORE_SPEEDHACKS, "vuThread", "enabled" if NbCpu > 3 else "disabled")
# Save configuration
vm.saveFile()
@staticmethod
def __ConfigureUI(system: Emulator, rom: str):
# Game Ratio
GAME_RATIO: Dict[str, str] = \
{
"16/9": "16:9",
"4/3": "4:3",
}
gameRatio = GAME_RATIO[system.Ratio] if system.Ratio in GAME_RATIO else "4:3"
# override UI Configuration
ui = IniSettings(Pcsx2Generator.pcsx2ConfigFilePCSX2_ui, True)
ui.defineBool("1", "0").loadFile(True)
ui.setString(Pcsx2Generator.SECTION_UI_NONE, "AskOnBoot", "disabled")
ui.setString(Pcsx2Generator.SECTION_UI_GSWINDOWS, "AspectRatio", gameRatio)
ui.setString(Pcsx2Generator.SECTION_UI_POPUPDISABLERS, "Dialog.ComponentsConfig.Overrides", "disabled,ok")
ui.setString(Pcsx2Generator.SECTION_UI_FILENAMES, "BIOS", Pcsx2Generator.__CheckPs2Bios(rom))''',
}
# dolphinGenerator.py:1-13, 113, 152-163, 228-273
DOLPHIN_BLOCKS = {
1: '''#!/usr/bin/env python3
from configgen.Command import Command
import configgen.recalboxFiles as recalboxFiles
from configgen.Emulator import Emulator
from configgen.controllers.controller import ControllerPerPlayer
from configgen.generators.Generator import Generator
from configgen.settings.keyValueSettings import keyValueSettings
from configgen.utils.Vulkan import Vulkan
from configgen.utils.architecture import Architecture
from configgen.utils.videoMode import createCrtDesktopRes, getCrtResolution, deleteCrtDesktopRes
class DolphinGenerator(Generator):''',
113: ''' SECTION_CORE = "Core"''',
152: ''' # Check if Gamecube bios IPL.bin in folders
@staticmethod
def CheckGamecubeIpl() -> str:
ipl0 = "/recalbox/share/bios/gamecube/EUR/IPL.bin"
ipl1 = "/recalbox/share/bios/gamecube/JP/IPL.bin"
ipl2 = "/recalbox/share/bios/gamecube/USA/IPL.bin"
# if os.path? return "True" set "False" for disable "SkipIpl=Video boot intro"
import os.path
if os.path.exists(ipl0) or os.path.exists(ipl1) or os.path.exists(ipl2):
return "False"
else:
return "True"''',
228: r''' def mainConfiguration(self, system: Emulator, args):
# Get Languages
language = self.GetLanguage()
systemLanguage = self.GetFrontendLanguage()
gamecubeLanguage = self.GAMECUBE_LANGUAGES[language] if language in self.GAMECUBE_LANGUAGES else 0
# Get Netplay configs
nickname = self.GetNetplay()
lobbyServer = self.NETPLAY_SERVERS[systemLanguage] if systemLanguage in self.NETPLAY_SERVERS else None
## Get if we have all IPL.bin in folders Gc bios
biosGamecube = self.CheckGamecubeIpl()
## set Gamecube Wii U adapter
padsGamecube = self.SetGamecubeWiiUAdapter()
# Get current rom path
import os
romName = os.path.basename(args.rom)
homeDir = args.rom.replace("/" + romName, "")
# Load Configuration
from configgen.settings.iniSettings import IniSettings
dolphinSettings = IniSettings(DolphinGenerator.iniFile, True)
dolphinSettings.loadFile(True) \
.defineBool('True', 'False')
# Resolution
from configgen.utils.resolutions import ResolutionParser
resolution = ResolutionParser(system.VideoMode)
if resolution.isSet and resolution.selfProcess:
dolphinSettings.setString(self.SECTION_DISPLAY, "FullscreenResolution", resolution.string)
dolphinSettings.setBool(self.SECTION_DISPLAY, "Fullscreen", True)
if Architecture().isX64:
dolphinSettings.setBool(self.SECTION_DISPLAY, "RenderToMain", True)
dolphinSettings.setBool(self.SECTION_DISPLAY, "KeepWindowOnTop", True)
# Logging
dolphinSettings.setBool(self.SECTION_DSP, "CaptureLog", args.verbose)
# Force audio backend on Pulse Audio
dolphinSettings.setString(self.SECTION_DSP, "Backend", "Pulse")
# Analytics
dolphinSettings.setBool(self.SECTION_ANALYTICS, "Enabled", True)
dolphinSettings.setBool(self.SECTION_ANALYTICS, "PermissionAsked", True)
# Bluetooth Pass-trough
dolphinSettings.setBool(self.SECTION_BTPASSTHROUGH, "Enabled", False)
# Core
dolphinSettings.setInt(self.SECTION_CORE, "SelectedLanguage", gamecubeLanguage)
dolphinSettings.setBool(self.SECTION_CORE, "WiimoteContinuousScanning", True)
dolphinSettings.setBool(self.SECTION_CORE, "WiiKeyboard", False)
dolphinSettings.setString(self.SECTION_CORE, "SkipIpl", biosGamecube)''',
}
# xemuGenerator.py:1-27, 30-60
XEMU_BLOCKS = {
1: """#!/usr/bin/env python3
import shutil
import os.path
from configgen.Command import Command
from configgen.Emulator import Emulator
from configgen.generators.Generator import Generator, ControllerPerPlayer
import configgen.recalboxFiles as recalboxFiles
from configgen.settings.keyValueSettings import keyValueSettings
from configgen.utils.Vulkan import Vulkan
from configgen.generators.xemu.xemuControllers import XemuControllers
class XemuGenerator(Generator):
SECTION_GENERAL = "general"
SECTION_SYS = "sys"
SECTION_SYS_FILES = "sys.files"
SECTION_AUDIO = "audio"
SECTION_DISPLAY = "display"
SECTION_DISPLAY_QUALITY = "display.quality"
SECTION_DISPLAY_UI = "display.ui"
SECTION_DISPLAY_WINDOW = "display.window"
SECTION_INPUT = "input"
SECTION_NET = "net"
savePath = recalboxFiles.SAVES + "/xbox"
biosPath = recalboxFiles.BIOS + "/xbox"
optionsConfigFile = recalboxFiles.CONF + '/xemu/xemu_config.ini'""",
30: ''' def mainConfiguration(self, system: Emulator, args):
# Load xemu config file defined in frontend options and get values
from configgen.settings.iniSettings import IniSettings
configSettings = IniSettings(self.optionsConfigFile, True)
configSettings.loadFile(True).defineBool('true', 'false')
skip_boot_anim = configSettings.getBool(self.SECTION_GENERAL, "skip_boot_anim", False)
show_menubar = configSettings.getBool(self.SECTION_DISPLAY_UI, "show_menubar", False)
show_notifications = configSettings.getBool(self.SECTION_DISPLAY_UI, "show_notifications", False)
fit = configSettings.getString(self.SECTION_DISPLAY_UI, "fit", "'scale'")
aspect_ratio = configSettings.getString(self.SECTION_DISPLAY_UI, "aspect_ratio", "'native'")
surface_scale = configSettings.getInt(self.SECTION_DISPLAY_QUALITY, "surface_scale", 1)
print(f"Xemu configuration: skip_boot_anim:{skip_boot_anim}")
# Load configuration
xemuSettings = IniSettings(self.xemuConfigFile, True)
xemuSettings.loadFile(True).defineBool('true', 'false')
# General
xemuSettings.setBool(self.SECTION_GENERAL, "skip_boot_anim", skip_boot_anim)
xemuSettings.setBool(self.SECTION_GENERAL, "show_welcome", False)
xemuSettings.setString(self.SECTION_GENERAL, "screenshot_dir", f"'{recalboxFiles.SCREENSHOTS}'")
# Sys
xemuSettings.setString(self.SECTION_SYS, "mem_limit", "'128'")
# Sys.files
xemuSettings.setString(self.SECTION_SYS_FILES, "flashrom_path", f"'{self.biosPath}/Complex_4627.bin'")
xemuSettings.setString(self.SECTION_SYS_FILES, "bootrom_path", f"'{self.biosPath}/mcpx_1.0.bin'")
xemuSettings.setString(self.SECTION_SYS_FILES, "hdd_path", f"'{self.savePath}/xbox_hdd.qcow2'")''',
}
# mupenConfig.py:1-5, 117-123
MUPEN_CONFIG_BLOCKS = {
1: '''#!/usr/bin/env python
import configgen.recalboxFiles as recalboxFiles
from configgen.Emulator import Emulator
from configgen.settings.iniSettings import IniSettings
from configgen.controllers.controller import InputItem, ControllerPerPlayer, Controller''',
117: '''def __setIpl64DD(system: Emulator, mupenSettings: IniSettings):
# Set ipl only for n64
section = '64DD'
if system.Name == "64dd":
mupenSettings.setString(section, "IPL-ROM", "/recalbox/share/bios/64DD_IPL.bin")
elif system.Name == "n64":
mupenSettings.setString(section, "IPL-ROM", "")''',
}
BLOCKS = {
FILES_MODULE: FILES_MODULE_BLOCKS,
LAUNCHER: LAUNCHER_BLOCKS,
RETROARCH: RETROARCH_BLOCKS,
CORES: CORES_BLOCKS,
LIBRETRO_GENERATOR: LIBRETRO_GENERATOR_BLOCKS,
HATARI: HATARI_BLOCKS,
GSPLUS: GSPLUS_BLOCKS,
AMIBERRY_KICKSTARTS: AMIBERRY_KICKSTARTS_BLOCKS,
AMIBERRY_SUBSYSTEMS: AMIBERRY_SUBSYSTEMS_BLOCKS,
AMIBERRY_CONFIG: AMIBERRY_CONFIG_BLOCKS,
PCSX2: PCSX2_BLOCKS,
DOLPHIN: DOLPHIN_BLOCKS,
XEMU: XEMU_BLOCKS,
MUPEN_CONFIG: MUPEN_CONFIG_BLOCKS,
}
def files(path: str | None = None, old: str = "", new: str = "") -> dict[str, str]:
"""The configgen tree, with `old` replaced once by `new` in `path`."""
tree = {name: placed(blocks) for name, blocks in BLOCKS.items()}
if path is not None:
if old not in tree[path]:
raise AssertionError(f"{old!r} is not in {path}")
tree[path] = tree[path].replace(old, new, 1)
return tree
def configgen(path: str | None = None, old: str = "", new: str = "") -> Configgen:
return Configgen(files(path, old, new).__getitem__)
def ref(path: str, lines: str) -> str:
return f"{path}:{lines}"
class SystemDirectoryPerCore(unittest.TestCase):
"""libretroRetroarch.py:431-529, the directory RetroArch is given."""
@classmethod
def setUpClass(cls) -> None:
cls.records = system_directories(configgen())
def by(self, emulator: str) -> list[Launcher]:
return [r for r in self.records if r.emulator == emulator]
def test_a_core_with_one_directory(self):
gpsp, = self.by("gpsp")
self.assertEqual(gpsp.kind, "passes_path")
self.assertEqual(gpsp.build, "libretro")
self.assertEqual(gpsp.names, [])
self.assertEqual(gpsp.search_dir, "/recalbox/share/bios/gba")
self.assertEqual(gpsp.passes_to, "retroarch system_directory")
self.assertIsNone(gpsp.depends_on_option)
self.assertEqual(gpsp.source_ref, ref(RETROARCH, "472-473"))
geargrafx, = self.by("geargrafx")
self.assertEqual(geargrafx.search_dir, "/recalbox/share/bios/pcenginecd")
def test_a_core_whose_directory_depends_on_the_system(self):
self.assertEqual(
[(r.search_dir, r.depends_on_option, r.source_ref) for r in self.by("bsnes")],
[
("/recalbox/share/bios/sgb", "system gb, gbc", ref(RETROARCH, "436-438")),
("/recalbox/share/bios/satellaview", "system satellaview",
ref(RETROARCH, "436, 439-440")),
("/recalbox/share/bios", "system other than gb, gbc, satellaview",
ref(RETROARCH, "436, 441-442")),
],
)
self.assertEqual(
[(r.search_dir, r.depends_on_option) for r in self.by("mesen_s")],
[("/recalbox/share/bios/sgb", "system flag SuperGameBoy"),
("/recalbox/share/bios/<system>", "without system flag SuperGameBoy")],
)
def test_a_directory_named_after_the_system(self):
picodrive, = self.by("picodrive")
self.assertEqual(picodrive.search_dir, "/recalbox/share/bios/<system>")
def test_every_other_core_gets_the_bios_root(self):
default = [r for r in self.by(ALL_CORES)
if r.depends_on_option == "core with no case of its own"]
self.assertEqual([(r.search_dir, r.source_ref) for r in default],
[("/recalbox/share/bios", ref(RETROARCH, "513-514"))])
def test_the_system_overrides_come_after_the_core(self):
overrides = [r for r in self.by(ALL_CORES) if r.depends_on_option.startswith("system")]
self.assertEqual(
[(r.search_dir, r.depends_on_option.removesuffix(
", replacing the directory chosen by core")) for r in overrides],
[
("/recalbox/share/bios/amiga/bios",
"system amiga600, amiga1200, amigacdtv, amigacd32"),
("/recalbox/share/bios/fds", "system fds"),
("/recalbox/share/bios/lynx", "system lynx"),
("/recalbox/share/bios/nds", "system nds"),
("/recalbox/share/bios/saturn", "system saturn"),
("/recalbox/share/bios/stv", "system stv"),
],
)
self.assertEqual(overrides[0].source_ref, ref(RETROARCH, "516-519"))
def test_the_cases_cover_the_match(self):
cores = {name for r in self.records for name in r.emulator.split(", ")}
self.assertEqual(len(cores - {ALL_CORES}), 29)
def test_dolphin_assets_directory(self):
assets, = core_assets(configgen())
self.assertEqual(assets.emulator, "dolphin")
self.assertEqual(assets.search_dir, "/recalbox/share/bios/dolphin-emu")
self.assertEqual(assets.passes_to, "retroarch core_assets_directory")
self.assertEqual(assets.source_ref, ref(RETROARCH, "423-424"))
def test_a_guarded_case_raises(self):
with self.assertRaises(LauncherShapeError):
system_directories(configgen(RETROARCH, "case 'gpsp':", "case 'gpsp' if True:"))
def test_a_branch_test_it_cannot_read_raises(self):
tree = configgen(RETROARCH, "elif self.system.Name == 'sufami':",
"elif self.system.Name.startswith('sufami'):")
with self.assertRaises(LauncherShapeError):
system_directories(tree)
def test_a_directory_it_cannot_spell_raises(self):
tree = configgen(RETROARCH, 'recalboxFiles.BIOS + "/gba"', "gbaDirectory()")
with self.assertRaises(LauncherShapeError):
system_directories(tree)
class HatariTosCascade(unittest.TestCase):
"""hatariGenerator.py:103-167 and libretroCores.py:165-298."""
def test_the_standalone_tries_the_machine_then_three_fallbacks(self):
records = hatari(configgen())
self.assertEqual(
[(r.slot, r.names[0]) for r in records[:-1]],
[("st", "atarist/st.img"), ("megast", "atarist/st.img"),
("ste", "atarist/ste.img"), ("megaste", "atarist/megaste.img"),
("tt", "atarist/tt.img"), ("falcon", "atarist/falcon.img")],
)
for record in records[:-1]:
self.assertEqual(record.kind, "names_searched")
self.assertEqual(record.names[1:], ["atarist/tos.img", "tos.img", "atarist/emutos.img"])
self.assertEqual(record.search_dir, "/recalbox/share/bios")
self.assertEqual(record.passes_to, "[ROM] szTosImageFileName")
self.assertIs(record.fails_when_missing, False)
self.assertEqual(record.source_ref, ref(HATARI, "108-138, 151-167"))
# coreSettings is never bound in SetMachineAndBios: no option fallback.
self.assertEqual(record.depends_on_option,
"machine read from the rom path through machines")
def test_emutos_in_the_rom_path_forces_one_name(self):
emutos = hatari(configgen())[-1]
self.assertEqual((emutos.kind, emutos.slot, emutos.names),
("fixed_name", "emutos", ["atarist/emutos.img"]))
self.assertEqual(emutos.depends_on_option, '"emutos" in the rom path')
def test_the_two_libretro_cores(self):
records = hatari_libretro(configgen())
old = [r for r in records if r.emulator == "hatari"]
new = [r for r in records if r.emulator == "hatarib"]
self.assertEqual([r.slot for r in old],
["st", "ste", "tt", "falcon", "megaste", "emutos"])
self.assertEqual(old[0].passes_to, "[ROM] szTosImageFileName")
self.assertEqual(old[0].source_ref, ref(CORES, "235-239, 249-267, 280-294"))
self.assertEqual([r.slot for r in new],
["st", "megast", "ste", "megaste", "tt", "falcon"])
self.assertEqual(new[0].names, ["atarist/st.img", "atarist/tos.img", "tos.img"])
self.assertEqual(new[0].passes_to,
"core option hatarib_tos (<etos1024k> when none is found)")
self.assertEqual(new[0].depends_on_option,
"machine read from the rom path through machines, "
"else option hatarib_machine")
self.assertEqual(new[0].source_ref, ref(CORES, "168-198, 211-212, 214-229"))
def test_another_existence_test_raises(self):
tree = configgen(HATARI, "if not os.path.exists(biosFile):",
"if not os.path.isfile(biosFile):")
with self.assertRaises(LauncherShapeError):
hatari(tree)
class GsplusRom(unittest.TestCase):
"""gsplusGenerator.py:93-148."""
def test_the_search_and_the_game_file_placeholders(self):
searched, rom1, rom3 = gsplus(configgen())
self.assertEqual(searched.names, ["apple2gs1.rom", "apple2gs3.rom"])
self.assertEqual(searched.kind, "names_searched")
self.assertEqual(searched.passes_to, "g_cfg_rom_path")
self.assertEqual(searched.depends_on_option, 'rom path without ".gsplus"')
self.assertEqual(searched.source_ref, ref(GSPLUS, "140-148"))
self.assertEqual([(r.slot, r.names) for r in (rom1, rom3)],
[("%1", ["apple2gs1.rom"]), ("%3", ["apple2gs3.rom"])])
self.assertEqual(rom1.source_ref, ref(GSPLUS, "107-108"))
def test_a_last_miss_that_keeps_the_key_raises(self):
tree = configgen(GSPLUS, 'settings.removeOption("g_cfg_rom_path")', "pass")
with self.assertRaises(LauncherShapeError):
gsplus(tree)
class AmiberryKickstarts(unittest.TestCase):
"""amiberryKickstarts.py:47-143 and amiberryConfig.py:337-345."""
def test_each_system_and_rom_type(self):
records = amiberry(configgen())
self.assertEqual(
[(r.slot, r.kind, r.names, r.fails_when_missing) for r in records],
[
("amiga600", "names_searched",
["kick37175.A500", "kick34005.A500", "kick33180.A500"], False),
("amiga600", "names_searched", ["kick34005.A500", "kick33180.A500"], False),
("amiga1200", "names_searched",
["kick40068.A1200", "kick39106.A1200", "kick40068.A4000"], False),
("amigacd32", "fixed_name", ["kick40060.CD32", "kick40060.CD32.ext"],
"No BIOS found, Extended BIOS not found"),
("amigacdtv", "fixed_name", ["kick34005.A500", "kick34005.CDTV"],
"No BIOS found, Extended BIOS not found"),
("amiga600, amiga1200", "fixed_name", ["kick02019.AROS", "kick02019.AROS.ext"],
False),
],
)
self.assertEqual(records[1].depends_on_option, "rom type DISK")
self.assertEqual(records[0].search_dir, "/recalbox/share/bios/amiga/bios")
self.assertEqual(records[0].passes_to, "kickstart_rom_file")
self.assertEqual(records[-1].passes_to,
"kickstart_rom_file, kickstart_ext_rom_file "
"(:AROS built in when either is missing)")
self.assertEqual(records[0].source_ref,
f"{ref(AMIBERRY_KICKSTARTS, '9-10, 50-53, 94-142')}, "
f"{ref(AMIBERRY_CONFIG, '337-345')}")
def test_several_pairs_for_one_rom_type_raise(self):
tree = configgen(AMIBERRY_KICKSTARTS, '["kick40060.CD32|kick40060.CD32.ext"]',
'["kick40060.CD32|kick40060.CD32.ext", "a|b"]')
with self.assertRaises(LauncherShapeError):
amiberry(tree)
class Pcsx2Region(unittest.TestCase):
"""pcsx2Generator.py:36-40, 55-107, 166."""
def test_the_region_token_comes_first_then_a_fixed_order(self):
records = pcsx2(configgen())
eu = records[0]
self.assertEqual(eu.slot, "EU")
self.assertEqual(eu.names, ["ps2-0230e-20080220.bin", "ps2-0230a-20080220.bin",
"ps2-0230j-20080220.bin", "ps2-0230h-20080220.bin"])
self.assertTrue(eu.depends_on_option.startswith("rom name contains europe, france"))
self.assertTrue(eu.depends_on_option.endswith(", uk"))
self.assertEqual(eu.search_dir, "/recalbox/share/bios/ps2")
self.assertEqual(eu.passes_to, "[Filenames] BIOS")
self.assertEqual(eu.source_ref, ref(PCSX2, "37-40, 56-107, 166"))
self.assertEqual([r.slot for r in records], ["EU", "USA", "JP", "HK", "default"])
def test_the_chinese_name_is_written_when_the_japanese_file_exists(self):
hk = pcsx2(configgen())[3]
self.assertEqual((hk.kind, hk.names), ("fixed_name", ["ps2-0230h-20080220.bin"]))
self.assertEqual(hk.depends_on_option,
"rom name contains china, taiwan and ps2-0230j-20080220.bin is present")
def test_a_region_branch_of_another_shape_raises(self):
tree = configgen(PCSX2, 'elif "uk" in lrom and Exists', 'elif "uk" in lrom or Exists')
with self.assertRaises(LauncherShapeError):
pcsx2(tree)
class FixedPaths(unittest.TestCase):
def test_xemu_writes_two_paths(self):
record, = xemu(configgen())
self.assertEqual(record.names, ["xbox/Complex_4627.bin", "xbox/mcpx_1.0.bin"])
self.assertEqual(record.passes_to, "[sys.files] flashrom_path, [sys.files] bootrom_path")
self.assertEqual(record.source_ref, ref(XEMU, "16, 26, 58-59"))
def test_mupen64plus_writes_the_64dd_ipl_for_one_system(self):
record, = mupen64plus(configgen())
self.assertEqual(record.names, ["64DD_IPL.bin"])
self.assertEqual(record.passes_to, "[64DD] IPL-ROM")
self.assertEqual(record.depends_on_option, "system 64dd")
self.assertEqual(record.source_ref, ref(MUPEN_CONFIG, "118-121"))
def test_dolphin_only_tests_the_ipl_for_presence(self):
standalone, libretro = dolphin(configgen())
names = ["gamecube/EUR/IPL.bin", "gamecube/JP/IPL.bin", "gamecube/USA/IPL.bin"]
self.assertEqual((standalone.emulator, standalone.names), ("dolphin, dolphin-gui", names))
self.assertEqual(standalone.passes_to, "[Core] SkipIpl (False when one is present, else True)")
self.assertEqual(standalone.source_ref, ref(DOLPHIN, "154-163, 236-237, 273"))
self.assertEqual((libretro.emulator, libretro.names), ("dolphin", names))
self.assertEqual(libretro.passes_to,
"core option dolphin_skip_gc_bios (disabled when one is present, "
"else enabled)")
self.assertEqual(libretro.source_ref, ref(CORES, "496-504"))
def test_minivmac_boot_disk_goes_through_a_command_file(self):
record, = content_files(configgen())
self.assertEqual((record.emulator, record.names),
("minivmac", ["macintosh/MinivMacBootv2.dsk"]))
self.assertEqual(record.passes_to,
"written into /tmp/minivmac.cmd, which is loaded as the content")
self.assertEqual(record.source_ref,
f"{ref(LIBRETRO_GENERATOR, '877-882')}, {ref(FILES_MODULE, '174')}")
def test_o2em_bios_option_is_read_from_the_core_directory(self):
cg = configgen()
records = core_file_options(cg, system_directories(cg))
self.assertEqual(
[(r.names, r.search_dir, r.depends_on_option) for r in records],
[(["o2rom.bin"], "/recalbox/share/bios/o2em", "system odyssey2"),
(["g7400.bin"], "/recalbox/share/bios/o2em", "system videopacplus")],
)
self.assertEqual(records[0].passes_to, "core option o2em_bios")
self.assertEqual(records[0].source_ref,
f"{ref(CORES, '350-351, 695')}, {ref(RETROARCH, '491-492')}")
class ExtractionNeverEmpty(unittest.TestCase):
def test_every_file_read_is_one_the_scraper_fetches(self):
tree = files()
read: list[str] = []
def reader(path: str) -> str:
read.append(path)
return tree[path]
records = extract_launchers(reader)
self.assertEqual(sorted(set(read)), sorted(SOURCE_FILES))
self.assertEqual(len(records), 85)
def test_records_fit_the_schema(self):
schema = json.loads((REPO_ROOT / "schemas" / "platform.schema.json").read_text())
launcher = schema["$defs"]["launcher"]
kinds = set(launcher["properties"]["kind"]["enum"])
for record in extract_launchers(files().__getitem__):
self.assertEqual(sorted(set(record) - set(launcher["properties"])), [])
self.assertEqual(sorted(set(launcher["required"]) - set(record)), [])
self.assertIn(record["kind"], kinds)
def test_a_files_module_without_bios_raises(self):
with self.assertRaises(LauncherShapeError):
configgen(FILES_MODULE, "BIOS = '/recalbox/share/bios'", "BIOS_DIR = ''")
def test_the_scraper_refuses_a_file_it_did_not_fetch(self):
tree = files()
scraper = recalbox_scraper.Scraper.__new__(recalbox_scraper.Scraper)
scraper.tag = "10.1.1"
fetched = {path: tree[path] for path in SOURCE_FILES if path != PCSX2}
with mock.patch.object(scraper, "_blobs", return_value=fetched), \
self.assertRaises(LauncherShapeError):
scraper._fetch_launchers()
def test_the_scraper_hands_over_what_it_fetched(self):
tree = files()
scraper = recalbox_scraper.Scraper.__new__(recalbox_scraper.Scraper)
scraper.tag = "10.1.1"
reader: Callable[[str], str] = tree.__getitem__
with mock.patch.object(scraper, "_blobs", return_value=tree):
self.assertEqual(scraper._fetch_launchers(), extract_launchers(reader))
class PlatformRecords(unittest.TestCase):
"""The launchers platforms/recalbox.yml carries, as the scraper wrote them."""
@classmethod
def setUpClass(cls) -> None:
cls.config = yaml.safe_load((REPO_ROOT / "platforms" / "recalbox.yml").read_text())
def test_every_emulator_is_one_recalbox_names(self):
cores = set(self.config["cores"]) | {ALL_CORES}
named = {
name for record in self.config["launchers"]
for name in record["emulator"].split(", ")
}
self.assertEqual(sorted(named - cores), [])
def test_every_reference_is_in_configgen(self):
for record in self.config["launchers"]:
for part in record["source_ref"].split(", "):
if not part[0].isdigit():
self.assertTrue(part.startswith(f"{CONFIGGEN}/"), record)
if __name__ == "__main__":
unittest.main()