mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-26 13:47:20 -05:00
feat: read elec ports and per-system cores
EmuELEC's ports system has one emulator, `ports`, the dispatch to a script ports.yaml generates. The scraper now reads each port's engine (the libretro core handed to emuelecRunEmu.sh with -C, else port_exe), so devilutionx, sonic2013, soniccd, sonic-mania, reminiscence and rigelengine are examined. Standalone emulators keep the spelling the launch code dispatches on instead of invented lowercase forms, and media players and runtimes go to the overrides' remove_cores. Each system of the AmberELEC, EmuELEC and KNULLI lists now carries the cores its ES list offers it, in menu order, under `cores:`.
This commit is contained in:
1 parent
d55a43bd68
commit
9648b9fc3b
12 files changed
+981
-179
No files matched your search
+127
-54
@@ -1,88 +1,161 @@
|
||||
"""The emulators an EmulationStation system list offers, by profile names.
|
||||
"""The emulators an EmulationStation system list offers, by system.
|
||||
|
||||
AmberELEC writes its list as es_systems.cfg XML, EmuELEC as an es_systems.json
|
||||
it turns into that XML at build time. Both name, per system, the emulators a
|
||||
user may pick and the cores under each. Some emulators are libretro front
|
||||
ends (RetroArch, retrorun) whose cores are libretro core names. Every other
|
||||
emulator is a standalone program named by the OS (`HATARISA`, `PPSSPPSDL`).
|
||||
it turns into that XML at build time. Both name, per system and in the order
|
||||
the menu shows them, the emulators a user may pick and the cores under each.
|
||||
Some emulators are libretro front ends (RetroArch, retrorun) whose cores are
|
||||
libretro core names. Every other emulator is a standalone program, named as
|
||||
the OS's launch code dispatches on it (`HATARISA`, `PPSSPPSDL`): profiles
|
||||
answer to that spelling in their `cores:`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from collections.abc import Iterable, Mapping
|
||||
|
||||
from safeparse import parse_untrusted_xml
|
||||
|
||||
# system -> [(emulator, [core, ...]), ...], both in the list's order.
|
||||
SystemEmulators = dict[str, list[tuple[str, list[str]]]]
|
||||
|
||||
|
||||
def xml_systems(text: str) -> SystemEmulators:
|
||||
"""Each system's emulators and cores, from an es_systems.cfg."""
|
||||
root = parse_untrusted_xml(text, "es_systems.cfg")
|
||||
systems: SystemEmulators = {}
|
||||
for system in root.iter("system"):
|
||||
name = (system.findtext("name") or "").strip()
|
||||
if not name:
|
||||
continue
|
||||
systems[name] = [
|
||||
(emulator.get("name"),
|
||||
[core.text.strip() for core in emulator.iter("core") if core.text])
|
||||
for emulator in system.iter("emulator")
|
||||
if emulator.get("name")
|
||||
]
|
||||
if not any(systems.values()):
|
||||
raise ValueError("es_systems.cfg names no emulator")
|
||||
return systems
|
||||
|
||||
|
||||
def json_systems(text: str) -> SystemEmulators:
|
||||
"""Each system's emulators and cores, from an es_systems.json."""
|
||||
systems: SystemEmulators = {}
|
||||
for system in json.loads(text).get("systems", []):
|
||||
systems[system["name"]] = [
|
||||
(emulator["name"], [core["name"] for core in emulator.get("cores") or []])
|
||||
for emulator in system.get("emulators") or []
|
||||
]
|
||||
if not any(systems.values()):
|
||||
raise ValueError("es_systems.json names no emulator")
|
||||
return systems
|
||||
|
||||
|
||||
def _pairs(systems: SystemEmulators) -> dict[str, set[str]]:
|
||||
pairs: dict[str, set[str]] = {}
|
||||
for emulators in systems.values():
|
||||
for emulator, cores in emulators:
|
||||
pairs.setdefault(emulator, set()).update(cores)
|
||||
return pairs
|
||||
|
||||
|
||||
def xml_emulators(text: str) -> dict[str, set[str]]:
|
||||
"""Emulator -> cores, from an es_systems.cfg."""
|
||||
root = parse_untrusted_xml(text, "es_systems.cfg")
|
||||
pairs: dict[str, set[str]] = {}
|
||||
for emulator in root.iter("emulator"):
|
||||
name = emulator.get("name")
|
||||
if name:
|
||||
pairs.setdefault(name, set()).update(
|
||||
core.text.strip() for core in emulator.iter("core") if core.text
|
||||
)
|
||||
if not pairs:
|
||||
raise ValueError("es_systems.cfg names no emulator")
|
||||
return pairs
|
||||
return _pairs(xml_systems(text))
|
||||
|
||||
|
||||
def json_emulators(text: str) -> dict[str, set[str]]:
|
||||
"""Emulator -> cores, from an es_systems.json."""
|
||||
pairs: dict[str, set[str]] = {}
|
||||
for system in json.loads(text).get("systems", []):
|
||||
for emulator in system.get("emulators") or []:
|
||||
pairs.setdefault(emulator["name"], set()).update(
|
||||
core["name"] for core in emulator.get("cores") or []
|
||||
)
|
||||
if not pairs:
|
||||
raise ValueError("es_systems.json names no emulator")
|
||||
return pairs
|
||||
return _pairs(json_systems(text))
|
||||
|
||||
|
||||
def standalone_names(emulator: str) -> list[str]:
|
||||
"""The names a profile may know a standalone emulator by.
|
||||
def _names(
|
||||
emulator: str,
|
||||
cores: Iterable[str],
|
||||
runners: Mapping[str, str | None],
|
||||
) -> list[str]:
|
||||
"""The core names one emulator entry stands for.
|
||||
|
||||
The OS's own spelling, its lowercase, and the lowercase without the
|
||||
`sa` suffix the ELEC family appends to a standalone build of an emulator
|
||||
that also exists as a core (`HATARISA`, `scummvmsa`, `yabasanshiroSA`).
|
||||
A runner's entry stands for the libretro cores it lists, or for the one
|
||||
core it always loads. A standalone emulator stands for itself.
|
||||
"""
|
||||
lower = emulator.lower()
|
||||
names = [emulator, lower]
|
||||
stripped = re.sub(r"-?sa$", "", lower)
|
||||
if stripped:
|
||||
names.append(stripped)
|
||||
return list(dict.fromkeys(names))
|
||||
if emulator in runners:
|
||||
fixed = runners[emulator]
|
||||
return [fixed] if fixed else list(cores)
|
||||
return [emulator]
|
||||
|
||||
|
||||
def system_cores(
|
||||
systems: SystemEmulators,
|
||||
runners: Mapping[str, str | None],
|
||||
not_cores: Iterable[str] = (),
|
||||
) -> dict[str, list[str]]:
|
||||
"""system -> the cores it runs, in the list's order, as the OS writes them."""
|
||||
dropped = set(not_cores)
|
||||
result: dict[str, list[str]] = {}
|
||||
for system, emulators in systems.items():
|
||||
names = [
|
||||
name
|
||||
for emulator, cores in emulators
|
||||
for name in _names(emulator, cores, runners)
|
||||
if name not in dropped
|
||||
]
|
||||
if names:
|
||||
result[system] = list(dict.fromkeys(names))
|
||||
return result
|
||||
|
||||
|
||||
def es_cores(
|
||||
pairs: dict[str, set[str]],
|
||||
runners: dict[str, str | None],
|
||||
renamed: dict[str, str] | None = None,
|
||||
systems: SystemEmulators,
|
||||
runners: Mapping[str, str | None],
|
||||
builds_of: Mapping[str, str] | None = None,
|
||||
not_cores: Iterable[str] = (),
|
||||
) -> tuple[list[str], list[str], list[str]]:
|
||||
"""(cores, standalone_cores, dual_mode_cores) of an ES system list.
|
||||
|
||||
`runners` maps each emulator that runs libretro cores to None when the
|
||||
cores it lists are libretro core names, or to the one core it always
|
||||
loads (AmberELEC's `mame` lists MAME machines and loads the MAME core).
|
||||
`renamed` gives the emulator a standalone name stands for when the
|
||||
suffix rule cannot (`PPSSPPSDL` is PPSSPP), each one read in the OS's
|
||||
launch code.
|
||||
`builds_of` maps a standalone emulator to the libretro core of the same
|
||||
emulator, each pair read in the OS's packages: the standalone one is
|
||||
dual-mode when the list also offers that core, as is a standalone name
|
||||
the list also offers as a core. `not_cores` names what
|
||||
the list files as an emulator without emulating anything (a media
|
||||
player, a runtime).
|
||||
"""
|
||||
renamed = renamed or {}
|
||||
builds_of = builds_of or {}
|
||||
dropped = set(not_cores)
|
||||
libretro: set[str] = set()
|
||||
for runner, fixed in runners.items():
|
||||
if runner in pairs:
|
||||
libretro.update([fixed] if fixed else pairs[runner])
|
||||
standalone: set[str] = set()
|
||||
for emulator in pairs:
|
||||
if emulator in runners:
|
||||
continue
|
||||
standalone.update(standalone_names(emulator))
|
||||
if emulator in renamed:
|
||||
standalone.add(renamed[emulator])
|
||||
dual = {name for name in standalone if name in libretro}
|
||||
for emulators in systems.values():
|
||||
for emulator, cores in emulators:
|
||||
names = {n for n in _names(emulator, cores, runners) if n not in dropped}
|
||||
(libretro if emulator in runners else standalone).update(names)
|
||||
dual = {
|
||||
name for name in standalone
|
||||
if name in libretro or builds_of.get(name) in libretro
|
||||
}
|
||||
return sorted(libretro | standalone), sorted(standalone), sorted(dual)
|
||||
|
||||
|
||||
def attach_system_cores(
|
||||
platform_systems: dict[str, dict], by_native: Mapping[str, list[str]]
|
||||
) -> None:
|
||||
"""Give each platform system the cores its native systems run.
|
||||
|
||||
A slug can gather several native systems (Diablo and Hellfire under
|
||||
diablo): its list is their union, in the order first met. A system the
|
||||
ES list does not name gets no list, as nothing says what runs it.
|
||||
"""
|
||||
for entry in platform_systems.values():
|
||||
natives = [entry.get("native_id")] + [
|
||||
f.get("native_system") for f in entry.get("files") or []
|
||||
]
|
||||
cores = [
|
||||
core
|
||||
for native in dict.fromkeys(n for n in natives if n)
|
||||
for core in by_native.get(native, [])
|
||||
]
|
||||
if cores:
|
||||
entry["cores"] = list(dict.fromkeys(cores))
|
||||
Reference in new issue
Block a user