feat: standalone emulator support for batocera and multi-platform name mapping

resolve_platform_cores() builds reverse index from profile cores: field,
fixing 17 name mismatches across Batocera, RetroBat, and Recalbox
(genesisplusgx, pce_fast, pcfx, vb, mame078plus, vice cores, etc.).

standalone_path field on file entries + standalone_cores on platform
YAMLs enable mode-aware pack generation. find_undeclared_files() uses
standalone_path for cores the platform runs standalone, filters by
mode: libretro/standalone per file.

batocera.yml gains standalone_cores (92 entries from configgen-defaults).
generate_readme.py dynamically lists platforms from registry.
3 profiles updated for standalone type/path (mame, hatari, mupen64plus_next).
78 E2E tests pass, pipeline verified.
This commit is contained in:
Abdessamad Derraz
2026-03-26 00:44:21 +01:00
parent 44dc946217
commit 3f676b75e8
31 changed files with 1492 additions and 40 deletions

View File

@@ -508,11 +508,20 @@ def resolve_platform_cores(
}
if isinstance(cores_config, list):
core_set = set(cores_config)
core_set = {str(c) for c in cores_config}
# Build reverse index: platform core name -> profile name
# Uses profile filename (dict key) + all names in cores: field
core_to_profile: dict[str, str] = {}
for name, p in profiles.items():
if p.get("type") == "alias":
continue
core_to_profile[name] = name
for core_name in p.get("cores", []):
core_to_profile[str(core_name)] = name
return {
name for name in profiles
if name in core_set
and profiles[name].get("type") != "alias"
core_to_profile[c]
for c in core_set
if c in core_to_profile
}
# Fallback: system ID intersection

View File

@@ -204,7 +204,7 @@ def _collect_emulator_extras(
if not u["in_repo"]:
continue
name = u["name"]
dest = name
dest = u.get("path") or name
full_dest = f"{base_dest}/{dest}" if base_dest else dest
if full_dest in seen:
continue

View File

@@ -109,8 +109,9 @@ def generate_readme(db: dict, platforms_dir: str) -> str:
lines = [
"# RetroBIOS",
"",
f"Complete BIOS and firmware packs for RetroArch, Batocera, Recalbox, Lakka,"
f" RetroPie, EmuDeck, RetroBat, and RetroDECK.",
f"Complete BIOS and firmware packs for "
f"{', '.join(c['platform'] for c in sorted(coverages.values(), key=lambda x: x['platform'])[:-1])}"
f", and {sorted(coverages.values(), key=lambda x: x['platform'])[-1]['platform']}.",
"",
f"**{total_files:,}** verified files across **{len(system_ids)}** systems,"
f" ready to extract into your emulator's BIOS directory.",

View File

@@ -127,8 +127,12 @@ class Scraper(BaseScraper):
def __init__(self, url: str = SOURCE_URL):
super().__init__(url=url)
def _fetch_cores(self) -> list[str]:
"""Extract core names from Batocera configgen-defaults.yml."""
def _fetch_cores(self) -> tuple[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".
"""
try:
req = urllib.request.Request(
CONFIGGEN_DEFAULTS_URL,
@@ -142,13 +146,19 @@ class Scraper(BaseScraper):
) from e
data = yaml.safe_load(raw)
cores: set[str] = set()
standalone: set[str] = set()
for system, cfg in data.items():
if system == "default" or not isinstance(cfg, dict):
continue
core = cfg.get("core")
emulator = cfg.get("emulator", "")
core = cfg.get("core", "")
if core:
cores.add(core)
return sorted(cores)
if emulator and emulator != "libretro":
standalone.add(emulator)
if core and core != emulator:
standalone.add(core)
return sorted(cores), sorted(standalone)
def _extract_systems_dict(self, raw: str) -> dict:
"""Extract and parse the 'systems' dict from the Python source via ast.literal_eval."""
@@ -295,7 +305,8 @@ class Scraper(BaseScraper):
if num.isdigit():
batocera_version = num
return {
cores, standalone = self._fetch_cores()
result = {
"platform": "Batocera",
"version": batocera_version or "",
"homepage": "https://batocera.org",
@@ -303,9 +314,12 @@ class Scraper(BaseScraper):
"base_destination": "bios",
"hash_type": "md5",
"verification_mode": "md5",
"cores": self._fetch_cores(),
"cores": cores,
"systems": systems,
}
if standalone:
result["standalone_cores"] = standalone
return result
def main():

View File

@@ -227,6 +227,7 @@ def find_undeclared_files(
profiles = emu_profiles if emu_profiles is not None else load_emulator_profiles(emulators_dir)
relevant = resolve_platform_cores(config, profiles)
standalone_set = set(str(c) for c in config.get("standalone_cores", []))
undeclared = []
seen = set()
for emu_name, profile in sorted(profiles.items()):
@@ -235,21 +236,36 @@ def find_undeclared_files(
if emu_name not in relevant:
continue
# Check if this profile is standalone: match profile name or any cores: alias
is_standalone = emu_name in standalone_set or bool(
standalone_set & {str(c) for c in profile.get("cores", [])}
)
for f in profile.get("files", []):
fname = f.get("name", "")
if not fname or fname in seen:
continue
# Skip standalone-only files for libretro platforms
if f.get("mode") == "standalone":
# Mode filtering: skip files incompatible with platform's usage
file_mode = f.get("mode")
if file_mode == "standalone" and not is_standalone:
continue
if file_mode == "libretro" and is_standalone:
continue
if fname in declared_names:
continue
# Determine destination path based on mode
if is_standalone:
dest = f.get("standalone_path") or f.get("path") or fname
else:
dest = f.get("path") or fname
in_repo = fname in by_name or fname.rsplit("/", 1)[-1] in by_name
seen.add(fname)
undeclared.append({
"emulator": profile.get("emulator", emu_name),
"name": fname,
"path": dest,
"required": f.get("required", False),
"hle_fallback": f.get("hle_fallback", False),
"category": f.get("category", "bios"),