fix: give a shared roms archive the default copy

A pack writes one file per path, and the second pass of core extras
gave a path read by several cores to the first profile in name order:
EmuELEC's arcade/neogeo.zip took AdvanceMAME's copy although the folder
starts MAME 2003-Plus by default (es_systems.json:82), and RetroDECK's
cps folders took MAME 2000's neogeo.zip and MAME 2003's nss.zip, which
lack members MAME 2003-Plus requires there.

The copies cores ask for at one path are now gathered first. The copy
of the core the roms folder starts by default goes first, then the one
holding every required member of the most claims, then profile order.
The others stay alternatives in the manifest.

Manifests: EmuELEC arcade/neogeo.zip becomes neogeo.zip.74df3258
(MAME 2003-Plus, AdvanceMAME's 4ccf6682 an alternative). RetroDECK
roms/cps, cps1, cps2 and cps3 get neogeo.zip.74df3258 and
nss.zip.cba0bd8c. No file is added or removed. verify --all keeps its
counts.
This commit is contained in:
Abdessamad Derraz committed 2026-11-11 12:55:22 +01:00
1 parent 852ce19ca9
commit 01a422f86c
2 files changed
+94 -4

No files matched your search

+57 -4
View File
@@ -10,7 +10,7 @@ from collections.abc import Iterable
from common import PROFILE_IDENTITY_FIELDS, _norm_system_id, entry_system_ids
from alternatives import core_file_spares, required_only_spares, set_fields
from common import archive_members, holds_required_members
from common import archive_members, contents_members, holds_required_members
from common import declared_root, expand_platform_declared_names, platform_declarations
from common import load_emulator_profiles
from common import resolution_is_hash_exact
@@ -1447,6 +1447,8 @@ def _collect_emulator_extras(
for e in extras:
covered_names.add(e["name"])
# Destination -> the copies cores ask for there, in profile order.
copied: dict[str, list[dict]] = {}
for emu_name, profile in sorted(profiles.items()):
if profile.get("type") in ("launcher", "alias"):
continue
@@ -1500,7 +1502,8 @@ def _collect_emulator_extras(
)
if full_dest in seen_dests and full_dest not in taken:
continue
if full_dest in taken and full_dest not in seen and fname not in platform_names:
if (full_dest in taken or full_dest in copied) and full_dest not in seen \
and fname not in platform_names:
# Another core's extra holds this path and only that
# extra covers the name: a pack without that core never
# reaches this copy, so this core claims nothing here.
@@ -1551,8 +1554,22 @@ def _collect_emulator_extras(
if f.get("layout_path"):
extra["layout_path"] = f["layout_path"]
extra["claim_order"] = (2, 0, emu_name)
if _take(extra, full_dest, seen_dests, taken):
extras.append(extra)
copied.setdefault(full_dest, []).append(extra)
# Several cores reading one archive in one roms folder: the copy of the
# core the folder starts by default goes first, then the copy serving
# the most of them. Five cores AmberELEC offers for roms/arcade read
# neogeo.zip there, and the copy holding the 18 members its default
# MAME 2003-Plus requires also holds the 3 MAME 2000 requires.
defaults = _folder_defaults(config, profiles)
for full_dest, claims in copied.items():
if len(claims) > 1:
claims = sorted(claims, key=lambda c: (
not _starts_by_default(c, defaults), -_copy_serves(c, claims, db),
))
for extra in claims:
if _take(extra, full_dest, seen_dests, taken):
extras.append(extra)
extras.extend(
_archive_prefix_extras(
@@ -1656,6 +1673,42 @@ class _ClaimRanks:
)
def _folder_defaults(config: dict, profiles: dict) -> dict[str, set[str]]:
"""Roms folder -> the profiles its default core or emulator names."""
out: dict[str, set[str]] = {}
for row in config.get("_rom_folders") or []:
chosen = {str(row.get("core") or ""), str(row.get("emulator") or "")} - {""}
keys = {c.lower().replace("-", "_") for c in chosen}
out[str(row.get("folder"))] = {
name for name, profile in profiles.items()
if keys & {n.lower().replace("-", "_")
for n in (name, *(str(c) for c in profile.get("cores") or []))}
}
return out
def _starts_by_default(extra: dict, defaults: dict[str, set[str]]) -> bool:
"""Whether the extra's core is the one its roms folder starts by default."""
layout = str(extra.get("layout_path") or "")
destination = str(extra.get("destination") or "")
if not layout or not destination.endswith("/" + layout):
return False
folder = destination[: -len(layout) - 1].removeprefix("roms/")
return extra.get("source_profile") in defaults.get(folder, set())
def _copy_serves(extra: dict, claims: list[dict], db: dict) -> int:
"""How many of the claims on one path the copy an extra resolves to
holds every required member of."""
wanted = [members for c in claims if (members := contents_members(c))]
if not wanted:
return 0
path, _status = resolve_local_file(extra, db, dest_hint=extra.get("destination", ""))
if not path:
return 0
return sum(1 for members in wanted if holds_required_members(path, members))
def _names_content(entry: dict) -> bool:
"""Whether a report entry declares the bytes it wants, not only a name.
+37
View File
@@ -1187,6 +1187,43 @@ class StandaloneMameOnEmuDeck(unittest.TestCase):
self.assertIn(("roms", "adam/adam.zip"), places)
def platform_manifest(platform: str) -> dict:
from common import load_database
db = load_database(str(REPO_ROOT / "database.json"))
with redirect_stdout(io.StringIO()):
return generate_manifest(
platform, str(REPO_ROOT / "platforms"), db, str(REPO_ROOT / "bios"),
str(REPO_ROOT / "platforms" / "_registry.yml"),
emulators_dir=str(REPO_ROOT / "emulators"), offline=True,
)
class ArchiveSharedByOfferedCores(unittest.TestCase):
"""One path holds one file: several cores reading an archive in one roms
folder get the copy of the core the folder starts by default, then the
copy serving the most of them."""
def test_the_default_core_copy_takes_the_folder(self) -> None:
# EmuELEC's arcade folder starts MAME 2003-Plus by default
# (es_systems.json:82), AdvanceMAME reads its sets there too.
(entry,) = [f for f in platform_manifest("emuelec")["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.assertIn("advancemame", 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
# alone lists members, and only its copy holds them.
(entry,) = [f for f in platform_manifest("retrodeck")["files"]
if f["dest"] == "roms/cps1/nss.zip"]
self.assertEqual(entry["cores"], ["mame2003_plus"])
self.assertTrue(entry["repo_path"].endswith("nss.zip.cba0bd8c"))
class MacBootDisksOnKnulli(unittest.TestCase):
def test_the_hard_records_bind(self) -> None:
# mameGenerator.py and libretroMAMEConfig.py pass the boot disk as