feat: record every core a rom folder offers

The rom folders files kept the default core of each folder only, so a
core the user picks in the frontend's menu was not known to read its
sets there. Each folder now lists its offers: every <core> of every
<emulator> in AmberELEC's es_systems.cfg (arcade:
packages/ui/emulationstation/config/es_systems.cfg:116-150), every core
of EmuELEC's es_systems.json, every add_emu_core of ROCKNIX's emulators
package.mk with the device case it is written under (arcade: 218-229),
and the emulator of every ES-DE <command>. A core written under every
branch of a case is offered unconditionally. The ES-XML, ES-JSON,
ELEC-conf and ES-DE setups are rescraped at their pins: archr, arkos,
chaos, darkos, jelos, kolos, pan4elec, retrobat and unofficialos gain
the same records and nothing else.

A libretro core offered for a folder reads the sets it loads beside the
game there, as the default one does.

Manifests, roms folders gaining sets read beside the game:
- AmberELEC: +215 (293.1 MB) in fbneo, arcade and mame, for MAME 2010,
  2015, 2016 and 2003-Plus. 1 omission leaves.
- EmuELEC: +291 (232.0 MB) in cps1, cps2, cps3, mame, fbneo and
  arcade, for MAME 2010 and 2003-Plus. 47 omissions leave.
- ROCKNIX: +501 (538.7 MB) in fbneo, neogeo, cps1, cps2, cps3, arcade
  and mame, for MAME 2010, 2015 and 2003-Plus. 120 omissions leave.
- EmuDeck: +306 (251.2 MB) in cps, cps1, cps2, cps3, arcade and mame,
  for MAME 2000, 2003, 2003-Plus and 2010. 92 omissions leave.
Each omission that leaves was a set named as read outside the BIOS
folder. RetroDECK and the other platforms do not change. No file
leaves, and verify --all keeps its severity and status counts.
This commit is contained in:
Abdessamad Derraz committed 2026-11-11 12:57:45 +01:00
1 parent 01a422f86c
commit 801af113b7
21 files changed
+19272 -6

No files matched your search

File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+15
View File
@@ -120,6 +120,21 @@
"type": "object",
"additionalProperties": { "$ref": "#/$defs/board_default" }
},
"offers": {
"type": "array",
"description": "Every emulator and core the frontend can start for this folder, the default included, in the list's order. Absent when the default is the only one.",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["core"],
"properties": {
"emulator": { "type": "string", "minLength": 1 },
"core": { "type": "string", "minLength": 1 },
"when": { "type": "string", "minLength": 1, "description": "The build condition the offer is written under (a device case)." },
"source_ref": { "$ref": "#/$defs/ref" }
}
}
},
"arcade": { "type": "boolean" },
"searched_by": {
"type": "array",
+7 -5
View File
@@ -3419,10 +3419,11 @@ def content_folder_tags(config: dict) -> set[str]:
A core reading its BIOS sets beside the game (MAME 2003's osd_get_path
gives FILETYPE_ROM the content directory) reads them in the folder of
the system it is launched for. The folders come from the platform's own
lists: ``libretro_systems``, the default core of each folder its rom
folders file records, the cores a system offers under its own name, and
the systems a RetroPie package registers. A platform with no roms root,
or one keeping no folder per system (RetroArch, Lakka), gives none.
lists: ``libretro_systems``, every core a folder of its rom folders file
offers (its default, its board defaults, ``offers``), the cores a system
offers under its own name, and the systems a RetroPie package registers.
A platform with no roms root, or one keeping no folder per system
(RetroArch, Lakka), gives none.
"""
if config.get("inherits_from") == "emulatorjs":
# EmulatorJS writes the BIOS a frontend hands it and the game to
@@ -3438,7 +3439,8 @@ def content_folder_tags(config: dict) -> set[str]:
for core, systems in (config.get("libretro_systems") or {}).items():
pairs |= {(_core_key(core), str(s)) for s in systems or []}
for row in rows:
for choice in [row, *(row.get("board_defaults") or {}).values()]:
choices = [row, *(row.get("board_defaults") or {}).values(), *(row.get("offers") or [])]
for choice in choices:
if choice.get("core") and choice.get("emulator") in LIBRETRO_RUNNERS:
pairs.add((_core_key(choice["core"]), str(row["folder"])))
for system in (config.get("systems") or {}).values():
+1 -1
View File
@@ -36,7 +36,7 @@ SETUP_KEYS = (
)
ROW_KEYS = (
"folder", "name", "aliases", "extensions", "files", "core", "emulator", "source_ref",
"board_defaults", "arcade", "searched_by", "note",
"board_defaults", "offers", "arcade", "searched_by", "note",
)
+57
View File
@@ -46,6 +46,44 @@ def _board(core: str | None, emulator: str | None, source_ref: str) -> dict:
return {k: str(v) for k, v in out.items() if v}
def _offers(choices: list[dict], core: str | None, emulator: str | None) -> list[dict]:
"""Every (emulator, core) a folder can start, in the list's order, or
none when the default is the only one.
A core written under several branches of a device case keeps the
conditions it is written under, and none once a branch is the `*)` one
beside another or no branch at all: every build then offers it.
"""
found: dict[tuple[str, str], dict] = {}
whens: dict[tuple[str, str], set[str]] = {}
lines: dict[tuple[str, str], list[str]] = {}
for choice in choices:
pair = (str(choice.get("emulator") or ""), str(choice.get("core") or choice.get("emulator") or ""))
if not pair[1]:
continue
whens.setdefault(pair, set()).add(str(choice.get("when") or ""))
ref = str(choice.get("source_ref") or "")
if ref and pair in found and found[pair].get("source_ref", "").split(":")[0] == ref.split(":")[0]:
lines[pair].append(ref.rsplit(":", 1)[-1])
if pair not in found:
found[pair] = {k: str(v) for k, v in {**choice, "core": pair[1]}.items()
if v and k != "when"}
lines[pair] = []
out = []
for pair, offer in found.items():
if lines[pair] and offer.get("source_ref"):
offer["source_ref"] = ",".join([offer["source_ref"], *lines[pair]])
conditions = whens[pair]
if "" not in conditions and not (
len(conditions) > 1 and any(w.endswith(" *") for w in conditions)):
offer = {**offer, "when": "; ".join(sorted(conditions))}
offer = {k: offer[k] for k in ("emulator", "core", "when", "source_ref") if k in offer}
out.append(offer)
if [(o.get("emulator", ""), o["core"]) for o in out] == [(emulator or "", core or emulator or "")]:
return []
return out
def _top_key_lines(text: str) -> dict[str, int]:
"""First line of each top-level YAML key."""
out: dict[str, int] = {}
@@ -286,6 +324,9 @@ def _xml_rows(text: str, key: str, path: str, params: dict) -> list[dict]:
"core": core or emulator,
"emulator": emulator,
"source_ref": _ref(key, path, row["line"]),
"offers": _offers(
[{"emulator": e, "core": c} for e, cores in emulators for c, _d in cores or [(e, False)]],
core, emulator),
"arcade": _is_arcade(system.findtext("hardware"), platform),
"note": row.get("note"),
})
@@ -322,6 +363,9 @@ def es_json(spec: dict, fetcher: Fetcher) -> list[dict]:
"core": core or emulator,
"emulator": emulator,
"source_ref": _ref(key, path, row["line"]),
"offers": _offers(
[{"emulator": e, "core": c} for e, cores in emulators for c, _d in cores or [(e, False)]],
core, emulator),
"arcade": _is_arcade(system.get("hardware"), system.get("platform")),
"note": row.get("note"),
})
@@ -470,6 +514,12 @@ def elec_conf(spec: dict, fetcher: Fetcher) -> list[dict]:
"emulator": pick["emulator"] if pick else None,
"source_ref": "; ".join(refs),
"board_defaults": board_defaults,
"offers": _offers([
{"emulator": c["emulator"], "core": c["core"],
"when": ", ".join(f"{v} {p}".strip() for v, p in c["branches"]),
"source_ref": _ref(key, package, c["line"])}
for c in mine
], pick and pick["core"], pick and pick["emulator"]),
"arcade": values.get("SYSTEM_HARDWARE") == "arcade" or None,
"note": note,
})
@@ -647,6 +697,7 @@ def esde(spec: dict, fetcher: Fetcher) -> list[dict]:
"folder": folder, "extensions": extensions, "commands": commands,
"ref": _ref(key, path, lines.get(name)),
"searched_by": _searched_by(placed, key, path),
"placed": [(*c, key, path) for c in placed],
}
alternatives: dict[str, tuple[str, int]] = {}
alt = params.get("alternatives")
@@ -669,6 +720,11 @@ def esde(spec: dict, fetcher: Fetcher) -> list[dict]:
else:
note = f"alternative emulator '{label}' names no command of this system"
emulator, core = _command_core(chosen[1])
offers = _offers([
{"emulator": e, "core": c, "source_ref": _ref(key_, path_, line)}
for _label, command, line, key_, path_ in system["placed"]
for e, c in [_command_core(command)] if e
], core, emulator)
extensions = system["extensions"]
if "." in extensions:
# ES-DE's "." is a file without an extension (SystemData.cpp
@@ -683,6 +739,7 @@ def esde(spec: dict, fetcher: Fetcher) -> list[dict]:
"emulator": emulator,
"source_ref": "; ".join(refs),
"note": note,
"offers": offers,
"searched_by": system["searched_by"] or None,
})
return rows
+31
View File
@@ -1187,6 +1187,27 @@ class StandaloneMameOnEmuDeck(unittest.TestCase):
self.assertIn(("roms", "adam/adam.zip"), places)
class EveryCoreAFolderOffers(unittest.TestCase):
"""AmberELEC's es_systems.cfg, EmuELEC's es_systems.json and ROCKNIX's
add_emu_core calls offer several cores per folder, the user's choice
starting any of them."""
def test_the_rom_folders_files_record_the_offers(self) -> None:
for setup, folder, core in (("amberelec", "arcade", "mame2010"),
("emuelec", "arcade", "mame2010"),
("rocknix", "arcade", "mame2010")):
with (REPO_ROOT / "platforms" / "romfolders" / f"{setup}.yml").open() as f:
rows = {r["folder"]: r for r in yaml.safe_load(f)["folders"]}
self.assertIn(core, {o["core"] for o in rows[folder].get("offers") or []}, setup)
def test_an_offered_core_reads_its_sets_in_that_folder(self) -> None:
for platform in ("amberelec", "rocknix"):
view = libretro_view("mame2010", platform)
folders = {e["path"].split("/")[0] for e in view["files"]
if e.get("category") == "bios_zip" and e.get("layout_path")}
self.assertIn("arcade", folders, platform)
def platform_manifest(platform: str) -> dict:
from common import load_database
@@ -1214,6 +1235,16 @@ class ArchiveSharedByOfferedCores(unittest.TestCase):
others = {c for a in entry.get("alternatives", []) for c in a["cores"]}
self.assertIn("advancemame", others)
def test_offered_cores_join_the_default_one(self) -> None:
# AmberELEC's arcade folder starts MAME 2003-Plus and offers MAME
# 2000, 2010, 2015 and 2016 (es_systems.cfg:116-150).
(entry,) = [f for f in platform_manifest("amberelec")["files"]
if f["dest"] == "arcade/neogeo.zip"]
self.assertEqual(entry["cores"], ["mame2003_plus"])
self.assertTrue(entry["repo_path"].endswith("neogeo.zip.74df3258"))
others = {c for a in entry.get("alternatives", []) for c in a["cores"]}
self.assertLessEqual({"mame2000", "mame2010", "mame2015", "mame2016"}, others)
def test_the_copy_serving_most_cores_wins_without_a_default(self) -> None:
# RetroDECK's cps folders start the libretro MAME core, whose sets are
# in mame/bios. Of the cores reading nss.zip in cps1, MAME 2003-Plus
+17
View File
@@ -133,6 +133,13 @@ class EsXmlFormat(unittest.TestCase):
self.assertEqual(rows["psx"]["board_defaults"]["rk3326"]["core"], "duckstation")
self.assertNotIn("board_defaults", rows["mame"])
def test_every_core_the_system_lists_is_offered(self) -> None:
self.assertEqual(self.rows["psx"]["offers"], [
{"emulator": "retroarch", "core": "pcsx_rearmed"},
{"emulator": "retroarch", "core": "swanstation"}])
# A default alone is no choice.
self.assertNotIn("offers", self.rows["mame"])
def test_source_ref_points_at_the_name_line(self) -> None:
self.assertEqual(self.rows["psx"]["source_ref"], "os es_systems.cfg:4")
self.assertEqual(self.rows["advision"]["source_ref"], "os es_systems.cfg:20")
@@ -173,6 +180,11 @@ class ElecConfFormat(unittest.TestCase):
"DEVICE=RK3326": {"core": "pcsx_rearmed", "emulator": "retroarch",
"source_ref": "os package.mk:11"}})
def test_a_core_under_every_branch_is_offered_unconditionally(self) -> None:
self.assertEqual(self.rows["psx"]["offers"], [
{"emulator": "retroarch", "core": "pcsx_rearmed", "source_ref": "os package.mk:11,16"},
{"emulator": "retroarch", "core": "swanstation", "source_ref": "os package.mk:12,15"}])
def test_unconditional_default(self) -> None:
self.assertEqual(self.rows["3do"]["core"], "opera")
self.assertEqual(self.rows["3do"]["source_ref"],
@@ -221,6 +233,11 @@ class EsdeFormat(unittest.TestCase):
self.assertTrue(libretro["any_machine"])
self.assertEqual(libretro["source_ref"], "os es_systems.xml:27")
def test_each_command_offers_its_emulator(self) -> None:
self.assertEqual(self.rows["arcade"]["offers"], [
{"emulator": "retroarch", "core": "mame", "source_ref": "os es_systems.xml:27"},
{"emulator": "mame", "core": "mame", "source_ref": "os es_systems.xml:28"}])
def test_first_command_without_alternative(self) -> None:
rows = by_folder(scrape("esde", "esde", {"systems": {"source": "os", "path": "es_systems.xml"}}))
self.assertEqual((rows["psx"]["core"], rows["psx"]["emulator"]), ("mednafen_psx", "retroarch"))