mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-11 12:33:53 -06:00
feat: record knulli's macintosh boot disk
MAME and libretro MAME pass /userdata/bios/<bootdisk>.img with -flop1
when the option is in the generator's list, else <bootdisk>.chd with
-hard. Each branch is a {bootdisk} template: the list the code tests
for the floppy, and for the hard disk the choices es_features.yml
offers that the list does not take. The menu offers macos3 while the
code tests macos30, so System 3.0 is passed as macos3.chd.
This commit is contained in:
1 parent
f0289e3624
commit
7ca7872fdc
3 files changed
+286
No files matched your search
@@ -5764,6 +5764,88 @@ launchers:
|
||||
fails_when_missing: Please add the Raspberry Pi version of Pico-8 to roms/pico8
|
||||
and try again.
|
||||
source_ref: "board/fsoverlay/usr/bin/knulli-install-pico8:3-11, 16-19, 66-69"
|
||||
- emulator: mame
|
||||
build: standalone
|
||||
kind: fixed_name
|
||||
names:
|
||||
- "{bootdisk}.img"
|
||||
expands:
|
||||
bootdisk:
|
||||
- macos30
|
||||
- macos608
|
||||
- macos701
|
||||
- macos75
|
||||
case_insensitive: false
|
||||
search_dir: /userdata/bios
|
||||
passes_to: command line -flop1
|
||||
fails_when_missing: false
|
||||
depends_on_option: "bootdisk (not (messSysName[messMode] == '' or messMode == -1)\
|
||||
\ and softList == '' and system.name == 'macintosh' and system.isOptSet('bootdisk')\
|
||||
\ and system.config['bootdisk'] in ['macos30', 'macos608', 'macos701', 'macos75'])"
|
||||
source_ref: "package/system/knulli-configgen/configgen/configgen/generators/mame/mameGenerator.py:303-305,\
|
||||
\ 375-379, 381, 385"
|
||||
- emulator: mame
|
||||
build: standalone
|
||||
kind: fixed_name
|
||||
names:
|
||||
- "{bootdisk}.chd"
|
||||
expands:
|
||||
bootdisk:
|
||||
- macos3
|
||||
- mac608
|
||||
- mac701
|
||||
- mac755
|
||||
case_insensitive: false
|
||||
search_dir: /userdata/bios
|
||||
passes_to: command line -hard
|
||||
fails_when_missing: false
|
||||
depends_on_option: "bootdisk (not (messSysName[messMode] == '' or messMode == -1)\
|
||||
\ and softList == '' and system.name == 'macintosh' and system.isOptSet('bootdisk')\
|
||||
\ and not (system.config['bootdisk'] in ['macos30', 'macos608', 'macos701', 'macos75']))"
|
||||
source_ref: "package/system/knulli-configgen/configgen/configgen/generators/mame/mameGenerator.py:303-305,\
|
||||
\ 375-379, 384-385, package/emulationstation/knulli-es-system/es_features.yml:11037-11043"
|
||||
- emulator: mame, mess, mamevirtual, same_cdi
|
||||
build: libretro
|
||||
kind: fixed_name
|
||||
names:
|
||||
- "{bootdisk}.img"
|
||||
expands:
|
||||
bootdisk:
|
||||
- macos30
|
||||
- macos608
|
||||
- macos701
|
||||
- macos75
|
||||
case_insensitive: false
|
||||
search_dir: /userdata/bios
|
||||
passes_to: command line -flop1
|
||||
fails_when_missing: false
|
||||
depends_on_option: "bootdisk (not (system.name in ['mame', 'neogeo', 'lcdgames',\
|
||||
\ 'plugnplay', 'vis']) and not (messSysName[messMode] == '') and not (softList\
|
||||
\ != '') and system.name == 'macintosh' and system.isOptSet('bootdisk') and system.config['bootdisk']\
|
||||
\ in ['macos30', 'macos608', 'macos701', 'macos75'])"
|
||||
source_ref: "package/system/knulli-configgen/configgen/configgen/generators/libretro/libretroMAMEConfig.py:64,\
|
||||
\ 123, 197, 265-268, 270, 274"
|
||||
- emulator: mame, mess, mamevirtual, same_cdi
|
||||
build: libretro
|
||||
kind: fixed_name
|
||||
names:
|
||||
- "{bootdisk}.chd"
|
||||
expands:
|
||||
bootdisk:
|
||||
- macos3
|
||||
- mac608
|
||||
- mac701
|
||||
- mac755
|
||||
case_insensitive: false
|
||||
search_dir: /userdata/bios
|
||||
passes_to: command line -hard
|
||||
fails_when_missing: false
|
||||
depends_on_option: "bootdisk (not (system.name in ['mame', 'neogeo', 'lcdgames',\
|
||||
\ 'plugnplay', 'vis']) and not (messSysName[messMode] == '') and not (softList\
|
||||
\ != '') and system.name == 'macintosh' and system.isOptSet('bootdisk') and not\
|
||||
\ (system.config['bootdisk'] in ['macos30', 'macos608', 'macos701', 'macos75']))"
|
||||
source_ref: "package/system/knulli-configgen/configgen/configgen/generators/libretro/libretroMAMEConfig.py:64,\
|
||||
\ 123, 197, 265-268, 273-274, package/emulationstation/knulli-es-system/es_features.yml:3727-3733"
|
||||
- emulator: dolphin
|
||||
build: standalone
|
||||
kind: fixed_name
|
||||
|
||||
@@ -26,6 +26,8 @@ from collections.abc import Callable, Iterator
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
try:
|
||||
from .batocera_launchers import (
|
||||
Configgen,
|
||||
@@ -79,6 +81,7 @@ CONFIGGEN = "package/system/knulli-configgen/configgen/configgen"
|
||||
GENERATORS = f"{CONFIGGEN}/generators"
|
||||
LIBRETRO_OPTIONS = f"{GENERATORS}/libretro/libretroOptions.py"
|
||||
PICO8_INSTALLER = "board/fsoverlay/usr/bin/knulli-install-pico8"
|
||||
ES_FEATURES = "package/emulationstation/knulli-es-system/es_features.yml"
|
||||
|
||||
# Batocera's extractors that read KNULLI's generators unchanged.
|
||||
SHARED = (
|
||||
@@ -909,6 +912,143 @@ def relative_folders(
|
||||
return launchers
|
||||
|
||||
|
||||
def _yaml_node(root: yaml.Node, keys: tuple[str, ...], path: str) -> yaml.Node:
|
||||
node = root
|
||||
for key in keys:
|
||||
if not isinstance(node, yaml.MappingNode):
|
||||
raise LauncherShapeError(f"{path}: {key} is not under a mapping")
|
||||
found = [v for k, v in node.value if isinstance(k, yaml.ScalarNode) and k.value == key]
|
||||
if len(found) != 1:
|
||||
raise LauncherShapeError(f"{path}: no {'/'.join(keys)}")
|
||||
node = found[0]
|
||||
return node
|
||||
|
||||
|
||||
def option_choices(read: Callable[[str], str], keys: tuple[str, ...]) -> tuple[list[str], tuple[int, int]]:
|
||||
"""The values es_features.yml offers for an option, and their lines."""
|
||||
node = _yaml_node(yaml.compose(read(ES_FEATURES)), (*keys, "choices"), ES_FEATURES)
|
||||
if not isinstance(node, yaml.MappingNode) or not node.value:
|
||||
raise LauncherShapeError(f"{ES_FEATURES}: {'/'.join(keys)} has no choices")
|
||||
values = [str(v.value) for _k, v in node.value if isinstance(v, yaml.ScalarNode)]
|
||||
return values, (node.start_mark.line + 1, node.end_mark.line)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BootDisk:
|
||||
"""A generator that names a BIOS file from an option value, and the ES feature offering it."""
|
||||
|
||||
path: str
|
||||
feature: tuple[str, ...]
|
||||
build: str = "standalone"
|
||||
caller: tuple[str, str] | None = None
|
||||
|
||||
|
||||
BOOT_DISKS = (
|
||||
BootDisk("mame/mameGenerator.py", ("mame", "systems", "macintosh", "cfeatures")),
|
||||
BootDisk(
|
||||
"libretro/libretroMAMEConfig.py",
|
||||
("libretro", "cores", "mame", "systems", "macintosh", "cfeatures"), "libretro",
|
||||
("libretro/libretroConfig.py", "generateMAMEConfigs"),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _option_template(reader: _Reader, node: ast.AST) -> tuple[str, str] | None:
|
||||
"""(option, name) for '<bios>/' + system.config[option] + '<suffix>'."""
|
||||
if not (isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add)):
|
||||
return None
|
||||
left, suffix = node.left, node.right
|
||||
if not (
|
||||
isinstance(left, ast.BinOp) and isinstance(left.op, ast.Add)
|
||||
and isinstance(left.left, ast.Constant) and isinstance(suffix, ast.Constant)
|
||||
and isinstance(left.right, ast.Subscript)
|
||||
and ast.unparse(left.right.value) == "system.config"
|
||||
and isinstance(left.right.slice, ast.Constant)
|
||||
):
|
||||
return None
|
||||
prefix = str(left.left.value).strip('"')
|
||||
if not prefix.startswith(reader.bios + "/"):
|
||||
return None
|
||||
option = str(left.right.slice.value)
|
||||
name = prefix[len(reader.bios) + 1:] + "{" + option + "}" + str(suffix.value).strip('"')
|
||||
return option, name
|
||||
|
||||
|
||||
def boot_disks(
|
||||
cg: Configgen, read: Callable[[str], str], specs: tuple[BootDisk, ...] = BOOT_DISKS
|
||||
) -> list[Launcher]:
|
||||
"""`if system.config[o] in [...]: X = bios + o + ext else: X = bios + o + ext2`."""
|
||||
launchers = []
|
||||
for spec in specs:
|
||||
src = cg.source(f"{cg.generators}/{spec.path}")
|
||||
reader = _Reader(cg, src)
|
||||
module = spec.path.removesuffix(".py").replace("/", ".")
|
||||
emulator = (
|
||||
", ".join(_caller_cores(cg, spec.caller)) if spec.caller else cg.emulator(module)
|
||||
)
|
||||
found = 0
|
||||
for chain in ast.walk(src.tree):
|
||||
if not (
|
||||
isinstance(chain, ast.If) and isinstance(chain.test, ast.Compare)
|
||||
and isinstance(chain.test.ops[0], ast.In)
|
||||
and isinstance(chain.test.comparators[0], ast.List)
|
||||
and ast.unparse(chain.test.left).startswith("system.config[")
|
||||
):
|
||||
continue
|
||||
branches = [(chain.body, True), (chain.orelse, False)]
|
||||
templates = []
|
||||
for body, listed in branches:
|
||||
flag = next(
|
||||
(str(s.value.value) for s in body if isinstance(s, ast.Assign)
|
||||
and isinstance(s.value, ast.Constant) and str(s.value.value).startswith("-")),
|
||||
None,
|
||||
)
|
||||
named = [
|
||||
(s, t) for s in body if isinstance(s, ast.Assign)
|
||||
and (t := _option_template(reader, s.value)) is not None
|
||||
]
|
||||
if flag is None or len(named) != 1:
|
||||
break
|
||||
templates.append((listed, flag, *named[0]))
|
||||
if len(templates) != 2:
|
||||
continue
|
||||
option = templates[0][3][0]
|
||||
if option != templates[1][3][0] or ast.unparse(chain.test.left) != f"system.config[{option!r}]":
|
||||
src.fail(f"line {chain.lineno} names a file from two options")
|
||||
listed_values = [
|
||||
str(e.value) for e in chain.test.comparators[0].elts if isinstance(e, ast.Constant)
|
||||
]
|
||||
offered, lines = option_choices(read, (*spec.feature, option))
|
||||
holder = reader.parents.get(id(chain))
|
||||
body = getattr(holder, "body", [])
|
||||
index = next(i for i, stmt in enumerate(body) if stmt is chain)
|
||||
added = body[index + 1] if index + 1 < len(body) else None
|
||||
if not (
|
||||
isinstance(added, ast.AugAssign) and isinstance(added.value, ast.List)
|
||||
and ast.unparse(added.target) in COMMAND_NAMES
|
||||
):
|
||||
src.fail(f"the file chosen at line {chain.lineno} is not put on the command line")
|
||||
for listed, flag, statement, (_, name) in templates:
|
||||
values = listed_values if listed else [v for v in offered if v not in listed_values]
|
||||
if not values:
|
||||
src.fail(f"no {option} value reaches the branch at line {statement.lineno}")
|
||||
tests, spans = reader.conditions(statement)
|
||||
found += 1
|
||||
launchers.append(Launcher(
|
||||
emulator=emulator, build=spec.build, kind="fixed_name", names=[name],
|
||||
expands={option: values}, case_insensitive=False, search_dir=cg.bios,
|
||||
passes_to=f"command line {flag}", fails_when_missing=False,
|
||||
depends_on_option=f"{option} ({' and '.join(tests)})",
|
||||
source_ref=", ".join([
|
||||
src.ref([*spans, src.span(statement), _lines(added)]),
|
||||
f"{ES_FEATURES}:{lines[0]}-{lines[1]}" if not listed else "",
|
||||
]).rstrip(", "),
|
||||
))
|
||||
if not found:
|
||||
src.fail("names no BIOS file from an option value")
|
||||
return launchers
|
||||
|
||||
|
||||
EXTRACTORS: tuple[Callable[[Configgen], list[Launcher]], ...] = (
|
||||
*SHARED, lexaloffle, core_file_options, relative_folders,
|
||||
)
|
||||
@@ -919,6 +1059,7 @@ def extract_launchers(read: Callable[[str], str]) -> list[dict[str, Any]]:
|
||||
cg = configgen(read)
|
||||
launchers = [launcher for extractor in EXTRACTORS for launcher in extractor(cg)]
|
||||
launchers += pico8_installer(read, cg.bios)
|
||||
launchers += boot_disks(cg, read)
|
||||
for spec in GENERIC_SITES:
|
||||
launchers += bios_sites(cg, spec)
|
||||
return [launcher.entry() for launcher in launchers]
|
||||
|
||||
@@ -60,6 +60,7 @@ IMPORTER = text(
|
||||
'hatari': ('hatari.hatariGenerator', 'HatariGenerator'),
|
||||
'melonds': ('melonds.melondsGenerator', 'MelonDSGenerator'),
|
||||
'pcsx2': ('pcsx2.pcsx2Generator', 'Pcsx2Generator'),
|
||||
'mame': ('mame.mameGenerator', 'MameGenerator'),
|
||||
}
|
||||
"""
|
||||
)
|
||||
@@ -329,6 +330,38 @@ PCSX2 = text(
|
||||
"""
|
||||
)
|
||||
|
||||
# mameGenerator.py: the Macintosh boot disk named from the bootdisk option.
|
||||
MAME = text(
|
||||
"""
|
||||
class MameGenerator(Generator):
|
||||
def generate(self, system, rom, playersControllers, metadata, guns, wheels, gameResolution):
|
||||
if system.name == "macintosh" and system.isOptSet("bootdisk"):
|
||||
if system.config["bootdisk"] in [ "macos30", "macos608" ]:
|
||||
bootType = "-flop1"
|
||||
bootDisk = "/userdata/bios/" + system.config["bootdisk"] + ".img"
|
||||
else:
|
||||
bootType = "-hard"
|
||||
bootDisk = "/userdata/bios/" + system.config["bootdisk"] + ".chd"
|
||||
commandArray += [ bootType, bootDisk ]
|
||||
"""
|
||||
)
|
||||
|
||||
FEATURES = text(
|
||||
"""
|
||||
mame:
|
||||
systems:
|
||||
macintosh:
|
||||
cfeatures:
|
||||
bootdisk:
|
||||
prompt: BOOT DISK
|
||||
choices:
|
||||
"System 3.0 (LD Floppy)": macos3
|
||||
"System 6.0.8 (LD Floppy)": macos608
|
||||
"System 7.5.5 (Hard Drive)": mac755
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def tree(**overrides: str) -> dict[str, str]:
|
||||
files = {
|
||||
f"{kl.CONFIGGEN}/batoceraPaths.py": PATHS,
|
||||
@@ -347,6 +380,8 @@ def tree(**overrides: str) -> dict[str, str]:
|
||||
f"{G}/hatari/hatariGenerator.py": HATARI,
|
||||
f"{G}/melonds/melondsGenerator.py": MELONDS,
|
||||
f"{G}/pcsx2/pcsx2Generator.py": PCSX2,
|
||||
f"{G}/mame/mameGenerator.py": MAME,
|
||||
kl.ES_FEATURES: FEATURES,
|
||||
}
|
||||
files.update(overrides)
|
||||
return files
|
||||
@@ -550,6 +585,34 @@ class CopiesAndFolders(unittest.TestCase):
|
||||
)
|
||||
|
||||
|
||||
class BootDisks(unittest.TestCase):
|
||||
def records(self, **overrides: str) -> list:
|
||||
files = tree(**overrides)
|
||||
return kl.boot_disks(kl.configgen(files.__getitem__), files.__getitem__, kl.BOOT_DISKS[:1])
|
||||
|
||||
def test_each_branch_is_a_template_over_the_values_that_reach_it(self):
|
||||
floppy, hard = self.records()
|
||||
self.assertEqual((floppy.names, floppy.passes_to), (["{bootdisk}.img"], "command line -flop1"))
|
||||
self.assertEqual(floppy.expands, {"bootdisk": ["macos30", "macos608"]})
|
||||
# The values the menu offers that the code's list does not take.
|
||||
self.assertEqual((hard.names, hard.passes_to), (["{bootdisk}.chd"], "command line -hard"))
|
||||
self.assertEqual(hard.expands, {"bootdisk": ["macos3", "mac755"]})
|
||||
self.assertTrue(hard.depends_on_option.startswith("bootdisk (system.name == 'macintosh'"))
|
||||
self.assertEqual(
|
||||
hard.source_ref,
|
||||
f"{G}/mame/mameGenerator.py:3-4, 9-10, {kl.ES_FEATURES}:8-10",
|
||||
)
|
||||
|
||||
def test_a_name_built_another_way_raises(self):
|
||||
broken = MAME.replace('"/userdata/bios/" + system.config["bootdisk"] + ".chd"', "disk")
|
||||
with self.assertRaises(LauncherShapeError):
|
||||
self.records(**{f"{G}/mame/mameGenerator.py": broken})
|
||||
|
||||
def test_an_option_the_menu_no_longer_offers_raises(self):
|
||||
with self.assertRaises(LauncherShapeError):
|
||||
self.records(**{kl.ES_FEATURES: FEATURES.replace("bootdisk:", "other:")})
|
||||
|
||||
|
||||
class PlatformFile(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
|
||||
Reference in new issue
Block a user