mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-18 09:23:26 -05:00
fix: lay out both builds of a dual-mode emulator
Batocera, RetroBat, Recalbox and ROCKNIX offer some emulators both as a libretro core and standalone. Filed as standalone only, the libretro layout was dropped: libretro Dolphin had no dolphin-emu/Sys/GC files. Platforms now name dual-mode emulators and each gets both layouts.
This commit is contained in:
1 parent
2b535525ef
commit
3c856bbc10
23 files changed
+1367
-478
No files matched your search
@@ -18,6 +18,7 @@ from pathlib import Path
|
||||
|
||||
|
||||
from common import yaml_load
|
||||
from safeparse import yaml_load_upstream
|
||||
|
||||
from .base_scraper import (
|
||||
BaseScraper,
|
||||
@@ -98,7 +99,7 @@ def es_systems_emulators(text: str) -> tuple[set[str], set[str]]:
|
||||
reading it alone left DOSBox Staging, melonDS DS and nine other built
|
||||
cores outside the pack.
|
||||
"""
|
||||
data = yaml_load(text)
|
||||
data = yaml_load_upstream(text)
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError("es_systems.yml is not a mapping")
|
||||
cores: set[str] = set()
|
||||
@@ -126,6 +127,20 @@ def es_systems_emulators(text: str) -> tuple[set[str], set[str]]:
|
||||
return cores, standalone
|
||||
|
||||
|
||||
def es_systems_libretro(text: str) -> set[str]:
|
||||
"""The cores es_systems.yml offers under `libretro:` for some system."""
|
||||
data = yaml_load_upstream(text)
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError("es_systems.yml is not a mapping")
|
||||
found: set[str] = set()
|
||||
for system in data.values():
|
||||
emulators = system.get("emulators") if isinstance(system, dict) else None
|
||||
libretro = emulators.get("libretro") if isinstance(emulators, dict) else None
|
||||
if isinstance(libretro, dict):
|
||||
found.update(str(core) for core, spec in libretro.items() if isinstance(spec, dict))
|
||||
return found
|
||||
|
||||
|
||||
SYSTEM_SLUG_MAP = {
|
||||
"advision": "entex-advision",
|
||||
"amiga500": "commodore-amiga",
|
||||
@@ -253,11 +268,12 @@ class Scraper(BaseScraper):
|
||||
self.tag = fetch_stable_tag()
|
||||
super().__init__(url=SOURCE_URL.format(tag=self.tag))
|
||||
|
||||
def _fetch_cores(self) -> tuple[list[str], list[str]]:
|
||||
def _fetch_cores(self) -> tuple[list[str], list[str], list[str]]:
|
||||
"""Extract core names and standalone cores from configgen-defaults.yml.
|
||||
|
||||
Returns (all_cores, standalone_cores) where standalone_cores are
|
||||
those with emulator != "libretro".
|
||||
Returns (all_cores, standalone_cores, dual_mode_cores): standalone
|
||||
cores are those with emulator != "libretro", dual-mode ones those
|
||||
es_systems.yml also offers under libretro (dolphin for GameCube).
|
||||
"""
|
||||
url = CONFIGGEN_DEFAULTS_URL.format(tag=self.tag)
|
||||
try:
|
||||
@@ -271,10 +287,10 @@ class Scraper(BaseScraper):
|
||||
raise ConnectionError(
|
||||
f"Failed to fetch {url}: {e}"
|
||||
) from e
|
||||
data = yaml_load(raw)
|
||||
cores, standalone = es_systems_emulators(
|
||||
self._fetch_text(ES_SYSTEMS_URL.format(tag=self.tag))
|
||||
)
|
||||
data = yaml_load_upstream(raw)
|
||||
es_systems = self._fetch_text(ES_SYSTEMS_URL.format(tag=self.tag))
|
||||
cores, standalone = es_systems_emulators(es_systems)
|
||||
libretro = es_systems_libretro(es_systems)
|
||||
if not cores:
|
||||
raise RuntimeError("es_systems.yml names no emulator")
|
||||
for system, cfg in data.items():
|
||||
@@ -289,7 +305,8 @@ class Scraper(BaseScraper):
|
||||
if core and core != emulator:
|
||||
standalone.add(core)
|
||||
dropped = _not_cores()
|
||||
return sorted(cores - dropped), sorted(standalone - dropped)
|
||||
dual = (standalone & libretro) - dropped
|
||||
return sorted(cores - dropped), sorted(standalone - dropped), sorted(dual)
|
||||
|
||||
def _fetch_text(self, url: str) -> str:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "retrobios-scraper/1.0"})
|
||||
@@ -449,7 +466,7 @@ class Scraper(BaseScraper):
|
||||
|
||||
systems[req.system]["files"].append(requirement_entry(req))
|
||||
|
||||
cores, standalone = self._fetch_cores()
|
||||
cores, standalone, dual = self._fetch_cores()
|
||||
result = {
|
||||
"platform": "Batocera",
|
||||
"version": self.tag.removeprefix("batocera-"),
|
||||
@@ -463,6 +480,9 @@ class Scraper(BaseScraper):
|
||||
}
|
||||
if standalone:
|
||||
result["standalone_cores"] = standalone
|
||||
if dual:
|
||||
# Launched both ways: each layout gets its files.
|
||||
result["dual_mode_cores"] = dual
|
||||
return result
|
||||
|
||||
|
||||
|
||||
@@ -153,6 +153,18 @@ def _core_names(values: dict[str, str]) -> tuple[set[str], set[str]]:
|
||||
return names, set(names)
|
||||
|
||||
|
||||
def system_ini_libretro(text: str) -> set[str]:
|
||||
"""The libretro cores one Recalbox system.ini offers."""
|
||||
import re
|
||||
|
||||
return {
|
||||
values.get("core", "")
|
||||
for section, values in _ini_sections(text).items()
|
||||
if re.fullmatch(r"core\.\d+(\.[\w-]+)?", section)
|
||||
and values.get("emulator") == "libretro" and values.get("core")
|
||||
}
|
||||
|
||||
|
||||
def system_ini_cores(text: str) -> tuple[set[str], set[str]]:
|
||||
"""The cores one Recalbox system.ini offers, and those run standalone.
|
||||
|
||||
@@ -227,8 +239,9 @@ class Scraper(BaseScraper):
|
||||
raise RuntimeError(f"no system.ini at {self.tag} for {', '.join(missing[:5])}")
|
||||
return [texts[path] for path in paths]
|
||||
|
||||
def _fetch_cores(self) -> tuple[list[str], list[str]]:
|
||||
"""Every core Recalbox's systems offer, and those it runs standalone.
|
||||
def _fetch_cores(self) -> tuple[list[str], list[str], list[str]]:
|
||||
"""Every core Recalbox's systems offer, those it runs standalone, and
|
||||
those it offers both as a libretro core and standalone.
|
||||
|
||||
Read from each system.ini of recalbox-romfs2 at the scraped tag: the
|
||||
systems ES lists and the cores a user can pick for each.
|
||||
@@ -250,13 +263,15 @@ class Scraper(BaseScraper):
|
||||
raise RuntimeError(f"no Recalbox system found at {self.tag}")
|
||||
cores: set[str] = set()
|
||||
standalone: set[str] = set()
|
||||
libretro: set[str] = set()
|
||||
for text in self._system_inis(systems):
|
||||
found, alone = system_ini_cores(text)
|
||||
cores |= found
|
||||
standalone |= alone
|
||||
libretro |= system_ini_libretro(text)
|
||||
if not cores:
|
||||
raise RuntimeError(f"no core in Recalbox's system.ini files at {self.tag}")
|
||||
return sorted(cores), sorted(standalone)
|
||||
return sorted(cores), sorted(standalone), sorted(libretro & standalone)
|
||||
|
||||
def fetch_requirements(self) -> list[BiosRequirement]:
|
||||
"""Parse es_bios.xml and return BIOS requirements."""
|
||||
@@ -350,7 +365,7 @@ class Scraper(BaseScraper):
|
||||
|
||||
systems[req.system]["files"].append(requirement_entry(req))
|
||||
|
||||
cores, standalone = self._fetch_cores()
|
||||
cores, standalone, dual = self._fetch_cores()
|
||||
return {
|
||||
"platform": "Recalbox",
|
||||
"version": self.tag,
|
||||
@@ -361,6 +376,8 @@ class Scraper(BaseScraper):
|
||||
"verification_mode": "md5",
|
||||
"cores": cores,
|
||||
"standalone_cores": standalone,
|
||||
# Launched both ways: each layout gets its files.
|
||||
**({"dual_mode_cores": dual} if dual else {}),
|
||||
"systems": systems,
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,21 @@ ES_SYSTEMS_URL = (
|
||||
)
|
||||
|
||||
|
||||
def es_systems_dual(text: str) -> set[str]:
|
||||
"""Emulators es_systems.cfg offers both as a libretro core and standalone."""
|
||||
from common import parse_untrusted_xml
|
||||
|
||||
libretro: set[str] = set()
|
||||
standalone: set[str] = set()
|
||||
for emulator in parse_untrusted_xml(text, "es_systems.cfg").iter("emulator"):
|
||||
name = (emulator.get("name") or "").strip()
|
||||
if name == "libretro":
|
||||
libretro |= {(core.text or "").strip() for core in emulator.iter("core")} - {""}
|
||||
elif name:
|
||||
standalone.add(name)
|
||||
return libretro & standalone
|
||||
|
||||
|
||||
def es_systems_cores(text: str) -> tuple[set[str], set[str]]:
|
||||
"""Every core RetroBat's ES offers, and the emulators it runs standalone.
|
||||
|
||||
@@ -207,7 +222,9 @@ class Scraper(BaseScraper):
|
||||
req = urllib.request.Request(es_url, headers={"User-Agent": "retrobios-scraper/1.0"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
cores, standalone = es_systems_cores(resp.read().decode("utf-8"))
|
||||
es_text = resp.read().decode("utf-8")
|
||||
cores, standalone = es_systems_cores(es_text)
|
||||
dual = es_systems_dual(es_text)
|
||||
except urllib.error.URLError as e:
|
||||
raise ConnectionError(f"Failed to fetch {es_url}: {e}") from e
|
||||
if not cores:
|
||||
@@ -223,6 +240,8 @@ class Scraper(BaseScraper):
|
||||
"verification_mode": "md5",
|
||||
"cores": sorted(cores),
|
||||
"standalone_cores": sorted(standalone),
|
||||
# Launched both ways: each layout gets its files.
|
||||
**({"dual_mode_cores": sorted(dual)} if dual else {}),
|
||||
"systems": systems,
|
||||
}
|
||||
|
||||
|
||||
@@ -236,6 +236,9 @@ class Scraper(BaseScraper):
|
||||
# emulators ROCKNIX packages beside it.
|
||||
"cores": ["retroarch", *standalone],
|
||||
"standalone_cores": standalone,
|
||||
# RetroArch carries every libretro core beside them, so an
|
||||
# emulator with a libretro build is launched both ways.
|
||||
"dual_mode_cores": list(standalone),
|
||||
"systems": systems,
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ from datetime import datetime, timezone
|
||||
|
||||
import yaml
|
||||
|
||||
from common import yaml_load
|
||||
from safeparse import yaml_load_upstream
|
||||
|
||||
from . import BaseTargetScraper
|
||||
|
||||
@@ -244,7 +244,7 @@ def _parse_es_systems(text: str) -> dict[str, list[str]]:
|
||||
<core_name>: {requireAnyOf: [BR2_PACKAGE_FOO]}
|
||||
"""
|
||||
try:
|
||||
data = yaml_load(text)
|
||||
data = yaml_load_upstream(text)
|
||||
except yaml.YAMLError:
|
||||
return {}
|
||||
|
||||
|
||||
Reference in new issue
Block a user