From 2c76cc7df9acebfe8271e7f19c22843978a5a49d Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Mon, 9 Nov 2026 17:39:46 +0100 Subject: [PATCH 1/6] feat: read a search list as one need A core that loads the first of several names (yabause: saturn_bios.bin, sega_101.bin, mpr-17933.bin) needs one of them. Profiles wrote this as required on the first name and optional on the others, so verify and the gap report called saturn_bios.bin a missing required file while sega_101.bin was held, and a set held only through a name marked optional read as a need met by an optional file. Model: an explicit group on the file entry, mirroring the platform field. priority was not reused: it is a rank, read by slot.py, region.py and truth.py inside a (system, region) slot, and that slot is not the set. px68k-sdl ranks its IPL list and its font list each from 1 in one system, pom2 ranks one list per machine model, DuckStation ranks 105 images across every region and takes any of them. A rank cannot say where one list ends. priority stays the order inside a group. Readers, through alternatives.settle_sets: - verify --emulator and the emulator pack: a held member fills the set, the others leave the report, a set nobody holds is one gap under its first member naming the others. - find_undeclared_files (verify platform, core complement of packs and manifests): same rule, a platform declaration or launcher counts as answering the set. - cross_reference: excused members are set aside as other versions. - slot.py: ranks compare members of one set only. - archive members: a required set inside an archive is met by one. - truth: a member gives no requirement of its own, one_of and one_of_required carry the set, and the exporters and the diff leave such a line's required as the platform wrote it. - validate_schemas: members of a group share required. --- schemas/emulator.schema.json | 9 +- scripts/alternatives.py | 87 +++++++++++ scripts/common.py | 40 ++++- scripts/cross_reference.py | 29 +++- scripts/exporter/baseline.py | 7 + scripts/exporter/recalbox_exporter.py | 7 +- scripts/generate_pack.py | 33 +++- scripts/slot.py | 15 +- scripts/truth.py | 35 ++++- scripts/validate_schemas.py | 25 +++ scripts/verify.py | 134 +++++++++++++--- tests/test_e2e.py | 214 ++++++++++++++++++++++++++ wiki/profiling.md | 5 +- 13 files changed, 599 insertions(+), 41 deletions(-) create mode 100644 scripts/alternatives.py diff --git a/schemas/emulator.schema.json b/schemas/emulator.schema.json index dab1a1d7a..b6ac0b212 100644 --- a/schemas/emulator.schema.json +++ b/schemas/emulator.schema.json @@ -674,10 +674,17 @@ ] }, "priority": { - "description": "Preference order among the BIOS a system accepts, lowest first. DuckStation's FindBIOSImageInDirectory keeps the image whose priority is lower. A search list the code walks in order maps onto it as 1, 2, 3.", + "description": "Preference order among the BIOS a system accepts, lowest first. DuckStation's FindBIOSImageInDirectory keeps the image whose priority is lower. A search list the code walks in order maps onto it as 1, 2, 3. The order only: which entries stand in for one another is group.", "type": "integer", "minimum": 0 }, + "group": { + "description": "Entries of one profile sharing a group are one need: the code loads one of them (the first it finds, in priority order where the source states one) and holding any member fills it. required states the set's requirement and is the same on every member. A list puts a file in several sets, a fallback two machine models share.", + "oneOf": [ + {"type": "string", "minLength": 1}, + {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "string", "minLength": 1}} + ] + }, "region_check": { "type": "boolean" }, diff --git a/scripts/alternatives.py b/scripts/alternatives.py new file mode 100644 index 000000000..ef15290f5 --- /dev/null +++ b/scripts/alternatives.py @@ -0,0 +1,87 @@ +"""Entries a core accepts in place of one another. + +A profile writes ``group:`` on the entries of one search list. Yabause tries +saturn_bios.bin, sega_101.bin, then mpr-17933.bin and loads the first it +finds (libretro.c:1035-1049): it needs one of them and none in particular. +``required`` on a member states the set's requirement, and ``priority`` keeps +the order the code reads them in. + +Holding one member fills the set, so the other members are not missing. A +set nobody holds is one gap, reported under its first member, never one gap +per member. Every reader settles sets through ``settle_sets`` so that verify, +the gap report and the packs agree on what is missing. +""" + +from __future__ import annotations + +from collections.abc import Hashable, Iterable, Sequence + +_UNRANKED = 1 << 30 + + +def groups_of(entry: dict) -> tuple[str, ...]: + """The sets an entry belongs to: one name, or a list for a file two + machine models both fall back on.""" + value = entry.get("group") + if not value: + return () + values = value if isinstance(value, list) else [value] + return tuple(str(v) for v in values if v) + + +def set_keys(owner: Hashable, entry: dict) -> tuple[tuple, ...]: + """The keys of an entry's sets, scoped to its owner (a profile, or a + profile under one build).""" + return tuple((owner, group) for group in groups_of(entry)) + + +def slot_set_keys(owner: Hashable, entries: Iterable[dict]) -> tuple[tuple, ...]: + """The sets the entries on one destination belong to. A destination has + one outcome however many versions it lists, so its versions' sets are + settled together.""" + keys: list[tuple] = [] + for entry in entries: + keys.extend(k for k in set_keys(owner, entry) if k not in keys) + return tuple(keys) + + +def _rank(entry: dict, position: int) -> tuple[int, int]: + priority = entry.get("priority") + return (priority if isinstance(priority, int) else _UNRANKED, position) + + +def settle_sets( + members: Sequence[tuple[tuple[tuple, ...], dict, bool]], +) -> tuple[set[int], dict[int, list[str]]]: + """Which unheld members are excused, and which one stands for its set. + + ``members`` lists ``(set keys, entry, fills)`` in the order the profile + declares them, where ``fills`` says the member is held (or answered by a + declaration that reports it). Returns the positions to leave out of a + report and, for each set no member fills, the position kept as its one + gap with the names of the others. A member fills or not on its own: a + held one is never excused, and an unheld one stays reported when it + stands for at least one unfilled set. + """ + by_set: dict[tuple, list[int]] = {} + for position, (keys, _entry, _fills) in enumerate(members): + for key in keys: + by_set.setdefault(key, []).append(position) + + standing: dict[int, list[str]] = {} + for positions in by_set.values(): + if any(members[p][2] for p in positions): + continue + first = min(positions, key=lambda p: _rank(members[p][1], p)) + names = standing.setdefault(first, []) + for p in positions: + name = str(members[p][1].get("name", "")) + if p != first and name not in names: + names.append(name) + + excused = { + position + for position, (keys, _entry, fills) in enumerate(members) + if keys and not fills and position not in standing + } + return excused, standing diff --git a/scripts/common.py b/scripts/common.py index 968320769..2cef0d5d9 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -1267,7 +1267,39 @@ _archive_index: dict[int, tuple[list[dict], int, dict[str | None, list[dict]]]] def _member(f: dict) -> dict: - return {"crc32": str(f["crc32"]).lower(), "required": bool(f.get("required", True))} + member = {"crc32": str(f["crc32"]).lower(), "required": bool(f.get("required", True))} + if f.get("group"): + # One of the members sharing it is enough (alternatives.py). + member["group"] = f["group"] + return member + + +def _required_met(members: list[dict], held: Callable[[dict], bool]) -> tuple[int, bool]: + """How many requirements an archive meets, and whether it meets all. + + A required member is one requirement, and so is a required set of + alternatives, met when one of its members is held: Odin reads the + first of three Hikaru boot ROMs it finds in hikaru.zip. + """ + from alternatives import groups_of + + met = 0 + complete = True + sets: dict[str, bool] = {} + for m in members: + if not m["required"]: + continue + names = groups_of(m) + if names: + for name in names: + sets[name] = sets.get(name, False) or held(m) + continue + if held(m): + met += 1 + else: + complete = False + met += sum(sets.values()) + return met, complete and all(sets.values()) def archive_members(archive: str, files: list[dict]) -> list[dict]: @@ -1316,7 +1348,7 @@ def holds_required_members(path: str, members: list[dict]) -> bool: """Whether an archive holds every member a profile requires inside it.""" crcs = zip_member_crcs(path) names = zip_member_names(path) - return all(_member_held(m, crcs, names) for m in members if m["required"]) + return _required_met(members, lambda m: _member_held(m, crcs, names))[1] def contents_members(file_entry: dict) -> list[dict]: @@ -1360,8 +1392,8 @@ def _holding_members(paths: list[str], members: list[dict]) -> list[str]: def held(path: str) -> tuple[int, int]: crcs = zip_member_crcs(path) names = zip_member_names(path) - found = [m for m in members if _member_held(m, crcs, names)] - return sum(1 for m in found if m["required"]), len(found) + found = {id(m) for m in members if _member_held(m, crcs, names)} + return _required_met(members, lambda m: id(m) in found)[0], len(found) scores = {path: held(path) for path in paths} best = max(scores.values(), default=(0, 0)) diff --git a/scripts/cross_reference.py b/scripts/cross_reference.py index af7525a77..8866b3420 100644 --- a/scripts/cross_reference.py +++ b/scripts/cross_reference.py @@ -23,6 +23,7 @@ from collections import Counter from pathlib import Path sys.path.insert(0, os.path.dirname(__file__)) +from alternatives import set_keys, settle_sets from common import ( platform_arg, ProfileSelectionError, @@ -797,6 +798,9 @@ def _cross_reference_profile( # the profile's entries in their order, so a position names one entry. position = {id(f): i for view, _ in layouts for i, f in enumerate(view.get("files", []))} fates: dict[int, str] = {} + # Grouped entries met, as (set keys, entry, fills, the list holding + # its row, the row): settled once the profile is read. + set_records: list[tuple[tuple, dict, bool, list | None, dict | None]] = [] def fate(f: dict, what: str) -> None: key = position[id(f)] @@ -834,15 +838,18 @@ def _cross_reference_profile( # Collect unsourceable files separately (documented, not a gap) unsourceable_reason = f.get("unsourceable", "") + keys = set_keys((emu_name, is_standalone), f) if unsourceable_reason: seen_files.add(seen_key) fate(f, "unsourceable") - unsourceable_list.append({ + row = { "name": fname, "required": f.get("required", False), "reason": unsourceable_reason, "source_ref": f.get("source_ref", ""), - }) + } + unsourceable_list.append(row) + set_records.append((keys, f, False, unsourceable_list, row)) continue if outside_gap_scope(f, is_standalone): @@ -863,6 +870,7 @@ def _cross_reference_profile( if in_platform: seen_files.add(seen_key) fate(f, "declared") + set_records.append((keys, f, True, None, None)) covered.append({ "name": fname, "path": effective_path, @@ -955,6 +963,21 @@ def _cross_reference_profile( (emu_name, is_standalone, f, probe, entry) ) gaps.append(entry) + set_records.append((keys, f, in_repo, gaps, entry)) + + # One need per set: a member another one fills is not a gap, and a set + # nobody holds is one gap under its first member (alternatives.py), as + # verify and the packs count it. + grouped = [r for r in set_records if r[0]] + excused, standing = settle_sets([(keys, f, fills) for keys, f, fills, _l, _r in grouped]) + for at, (_keys, member, _fills, rows, set_row) in enumerate(grouped): + if set_row is None or rows is None: + continue + if at in standing: + set_row["alternatives"] = standing[at] + if at in excused: + rows.remove(set_row) + fates[position[id(member)]] = "other_versions" for ag in sorted(archive_gaps.values(), key=lambda e: e["name"]): gaps.append(ag) @@ -1333,6 +1356,8 @@ def print_report( fc = g.get("archive_file_count", 0) rc = g.get("archive_required_count", 0) archive_info = f" ({fc} files, {rc} required)" + if g.get("alternatives") and not g.get("in_repo"): + archive_info += f" (or {', '.join(g['alternatives'])})" print(f" {req} {g['name']} [{src}]{archive_info}{note}") total_gaps += gaps diff --git a/scripts/exporter/baseline.py b/scripts/exporter/baseline.py index af09d300f..66f5a3b82 100644 --- a/scripts/exporter/baseline.py +++ b/scripts/exporter/baseline.py @@ -264,6 +264,8 @@ class NativeFile: return bool(self.truth["required"]) if self.platform is not None and self.platform.get("required") is not None: return bool(self.platform["required"]) + if self.truth is not None and self.truth.get("one_of_required") is not None: + return bool(self.truth["one_of_required"]) return True @property @@ -645,10 +647,15 @@ def build_native_model( matched.filled.append(field_name) t_req = truth_entry.get("required") p_req = (matched.platform or {}).get("required") + # A file a core takes as one of a set has no requirement of + # its own: a platform line cannot say "one of these", so + # truth leaves its value as written rather than claim + # every member optional or every member needed. if ( t_req is not None and p_req is not None and bool(t_req) != bool(p_req) + and not (truth_entry.get("one_of_required") and not t_req) ): matched.corrections.append("required") report.required_corrected.append( diff --git a/scripts/exporter/recalbox_exporter.py b/scripts/exporter/recalbox_exporter.py index 161c9a9bb..640658bd8 100644 --- a/scripts/exporter/recalbox_exporter.py +++ b/scripts/exporter/recalbox_exporter.py @@ -189,16 +189,21 @@ class Exporter(BaseExporter): } - {""} readers: set[str] = set() requirers: set[str] = set() + set_readers: set[str] = set() for entry in entries: cores = set(entry.get("_cores") or ()) & named readers |= cores + if entry.get("one_of_required"): + set_readers |= cores & set(entry.get("one_of") or ()) required_by = entry.get("_required_by") if required_by is None: if entry.get("required") is not False: requirers |= cores else: requirers |= cores & set(required_by) - if not readers: + # A core that needs one file of a set needs none of them on its own, + # which a mandatory flag cannot say: the line keeps its value. + if not readers or (not requirers and set_readers): if fe.platform is not None and fe.platform.get("required") is not None: return bool(fe.platform["required"]) return fe.required diff --git a/scripts/generate_pack.py b/scripts/generate_pack.py index 75bba4923..17adf5979 100644 --- a/scripts/generate_pack.py +++ b/scripts/generate_pack.py @@ -36,6 +36,7 @@ import zipfile from pathlib import Path, PurePosixPath sys.path.insert(0, os.path.dirname(__file__)) +from alternatives import settle_sets, slot_set_keys from common import resolve_platform_cores as _platform_cores from common import ( installed_data_refs, @@ -1838,6 +1839,10 @@ def generate_emulator_pack( continue slots.setdefault(dest, []).append(fe) + # What each destination came to, settled by set once the loop + # is over: a member another member fills is not missing, and a + # set nobody holds is reported once (alternatives.py). + outcomes: list[tuple[list[dict], bool, Callable[[], None] | None]] = [] for dest, alternatives in slots.items(): # Name order, first file wins, case folded: verify replays # the rule and reports a later profile's file as missing. @@ -1923,17 +1928,25 @@ def generate_emulator_pack( held, packed_at.get(dest.lower()) ): # The file already at this path, asked for twice. + outcomes.append((alternatives, True, None)) continue - report_missing(alternatives[0]["name"], dest) + outcomes.append((alternatives, False, functools.partial( + report_missing, alternatives[0]["name"], dest + ))) continue if chosen is None: if refusal: rejected_files.append(refusal) + outcomes.append((alternatives, False, None)) elif all(fe.get("unsourceable") for fe in alternatives): - unsourceable_files.append(alternatives[0]["name"]) + outcomes.append((alternatives, False, functools.partial( + unsourceable_files.append, alternatives[0]["name"] + ))) else: - report_missing(alternatives[0]["name"], dest) + outcomes.append((alternatives, False, functools.partial( + report_missing, alternatives[0]["name"], dest + ))) continue fe, local_path, how = chosen @@ -1954,8 +1967,11 @@ def generate_emulator_pack( total_files += 1 if record is not None: record.files.append(dest) + outcomes.append((alternatives, True, None)) else: - missing_files.append(fe["name"]) + outcomes.append((alternatives, False, functools.partial( + missing_files.append, fe["name"] + ))) finally: if os.path.exists(tmp_path): os.unlink(tmp_path) @@ -1969,6 +1985,7 @@ def generate_emulator_pack( # (but allow same file to be packed under different destinations, # e.g., IPL.bin in GC/USA/ and GC/EUR/ from same source) dedup_key_hash = f"{os.path.realpath(local_path)}:{dest}" + outcomes.append((alternatives, True, None)) if dedup_key_hash in seen_hashes: continue seen_hashes.add(dedup_key_hash) @@ -1985,6 +2002,14 @@ def generate_emulator_pack( if record is not None: record.files.append(dest) + excused, _standing = settle_sets([ + (slot_set_keys(emu_name, entries), entries[0], fills) + for entries, fills, _report in outcomes + ]) + for position, (_entries, _fills, report) in enumerate(outcomes): + if report is not None and position not in excused: + report() + # The trees after the files, as in a platform pack: a dump the # profile declares wins over the copy a tree bundles at the same # path (Dolphin's real dsp_rom.bin over Sys/GC's free one). diff --git a/scripts/slot.py b/scripts/slot.py index d5df6ffe0..8ab85b58f 100644 --- a/scripts/slot.py +++ b/scripts/slot.py @@ -29,6 +29,8 @@ rank: the second core reads it by its own rule, which no rank expresses. from __future__ import annotations +from alternatives import set_keys + BIOS_CATEGORY = "bios" @@ -65,9 +67,11 @@ def build_slot_index(profiles: dict) -> dict[str, dict]: entry = index.setdefault( key, {"rank": None, "conflict": False, "regions": set(), - "emulators": [], "ranked_by": set(), "unranked_by": set()}, + "emulators": [], "ranked_by": set(), "unranked_by": set(), + "sets": set()}, ) entry["regions"] |= {str(r) for r in (f.get("region") or [])} + entry["sets"] |= set(set_keys(emu_name, f)) rank = f.get("priority") entry["unranked_by" if rank is None else "ranked_by"].add(emu_name) if rank is not None: @@ -123,7 +127,7 @@ def resolve_slot_drops( # A slot is a system AND a declared region: the Japanese and American # PlayStation BIOS are not alternatives to each other, and comparing their # ranks across regions would pick one territory's file for another's. - tiers: dict[tuple[str, tuple[str, ...]], list[tuple[str, int | None]]] = {} + tiers: dict[tuple[str, tuple[str, ...], tuple], list[tuple[str, int | None]]] = {} for group_id, members in groups.items(): for destination, name in members: if held is not None and destination not in held: @@ -132,11 +136,14 @@ def resolve_slot_drops( if entry is None: keep.add(destination) continue + # A rank orders the members of one set: px68k-sdl ranks its IPL + # and its font ROM lists each from 1, and the two never compete. tier = tuple(sorted(entry["regions"])) + sets = tuple(sorted(entry.get("sets") or ())) rank = None if entry.get("conflict") else entry["rank"] - tiers.setdefault((group_id, tier), []).append((destination, rank)) + tiers.setdefault((group_id, tier, sets), []).append((destination, rank)) - for (group_id, tier), candidates in tiers.items(): + for (group_id, tier, _sets), candidates in tiers.items(): if len(candidates) < 2: keep.update(dest for dest, _p in candidates) continue diff --git a/scripts/truth.py b/scripts/truth.py index d463368ac..fd466b4ca 100644 --- a/scripts/truth.py +++ b/scripts/truth.py @@ -18,6 +18,7 @@ from common import ( resolve_platform_cores, standalone_set as platform_standalone_set, ) +from alternatives import groups_of from hashing import compute_hashes from validation import ( filter_files_by_mode, @@ -276,11 +277,15 @@ def _fold_into(existing: dict, file_entry: dict, emu_name: str) -> None: existing["_source_refs"] = existing.get("_source_refs", set()) | {sr_key} else: existing.setdefault("_source_refs", set()) - if file_entry.get("required"): + if groups_of(file_entry): + _fold_set_member(existing, file_entry, emu_name) + elif file_entry.get("required"): existing["required"] = True # Which cores need it: `required` above is the union, true when # any core needs it, and a package of one core must not read it. existing["_required_by"] = existing.get("_required_by", set()) | {emu_name} + elif file_entry.get("required") is not None: + existing.setdefault("required", file_entry["required"]) _merge_hashes(existing, file_entry, emu_name, revision) # Merge non-hash data fields if existing lacks them. # A core that creates an entry without size/path/validation may be @@ -292,6 +297,21 @@ def _fold_into(existing: dict, file_entry: dict, emu_name: str) -> None: _merge_region(existing, file_entry.get("region")) +def _fold_set_member(entry: dict, file_entry: dict, emu_name: str) -> None: + """A core that takes this file as one of a set needs the set, not it. + + Yabause loads the first of saturn_bios.bin, sega_101.bin and + mpr-17933.bin it finds. None of the three is required on its own, so + the core stays out of `required` and `_required_by`, and `_one_of` + names it with `one_of_required` saying whether its set is required. + """ + entry["_one_of"] = entry.get("_one_of", set()) | {emu_name} + if file_entry.get("required", True): + entry["one_of_required"] = True + else: + entry.setdefault("one_of_required", False) + + def _merge_region(existing: dict, theirs: str | list | None) -> None: """The regions every reader selects the file for, the pack's own rule. @@ -379,7 +399,8 @@ def _merge_priority(existing: dict, theirs: int | None) -> None: def _new_truth_entry(file_entry: dict, emu_name: str, db: dict | None) -> dict: entry: dict = {"name": file_entry["name"]} - if file_entry.get("required") is not None: + grouped = bool(groups_of(file_entry)) + if file_entry.get("required") is not None and not grouped: entry["required"] = file_entry["required"] for field in _COPIED_FIELDS: val = file_entry.get(field) @@ -396,7 +417,9 @@ def _new_truth_entry(file_entry: dict, emu_name: str, db: dict | None) -> dict: if not file_entry.get("region"): entry["_region_void"] = True entry["_cores"] = {emu_name} - entry["_required_by"] = {emu_name} if file_entry.get("required") else set() + entry["_required_by"] = {emu_name} if file_entry.get("required") and not grouped else set() + if grouped: + _fold_set_member(entry, file_entry, emu_name) entry["_named_by"] = {emu_name} if file_entry.get("_opens_by_name", True) else set() entry["_brought_by"] = _brought(file_entry, emu_name) sr = file_entry.get("source_ref") @@ -848,6 +871,9 @@ def generate_platform_truth( fe.pop("_region_void", None) fe["_cores"] = sorted(fe.get("_cores", set())) fe["_required_by"] = sorted(fe.get("_required_by", set())) + one_of = fe.pop("_one_of", None) + if one_of: + fe["one_of"] = sorted(one_of) fe["_named_by"] = sorted(fe.get("_named_by", set())) fe["_brought_by"] = sorted(fe.get("_brought_by", set())) if not fe["_brought_by"]: @@ -950,6 +976,9 @@ def _diff_system(truth_sys: dict, scraped_sys: dict) -> dict: hash_mismatch.append(mismatch) t_req = t_entry.get("required") s_req = s_entry.get("required") + if t_entry.get("one_of_required") and not t_req: + # Needed as one of a set: no value of its own to compare. + t_req = None if t_req is not None and s_req is not None and t_req != s_req: required_mismatch.append( { diff --git a/scripts/validate_schemas.py b/scripts/validate_schemas.py index db84dbc06..0ff95263e 100644 --- a/scripts/validate_schemas.py +++ b/scripts/validate_schemas.py @@ -86,6 +86,7 @@ def _validate_yaml_directory( out.extend(_save_dir_without_library_name(path, data)) out.extend(_systems_outside_profile(path, data)) out.extend(_doubled_paths(path)) + out.extend(_group_requirement_split(path, data)) return out @@ -122,6 +123,30 @@ def _systems_outside_profile(path: Path, document: object) -> list[str]: return out +def _group_requirement_split(path: Path, document: object) -> list[str]: + """A group whose members disagree on required. + + The members of a group are one need, so required states the set's and + reads the same on each. A true on the first name only wrote the search + order into required, and the other names read as optional files. + """ + from alternatives import groups_of + + if not isinstance(document, dict): + return [] + seen: dict[str, set[bool]] = {} + for entry in document.get("files") or []: + if not isinstance(entry, dict): + continue + for group in groups_of(entry): + seen.setdefault(group, set()).add(bool(entry.get("required", True))) + return [ + f"{path.relative_to(ROOT)}: group {group} mixes required true and false" + for group, values in sorted(seen.items()) + if len(values) > 1 + ] + + def _checks_without_values(path: Path, document: object) -> list[str]: """A check an entry declares with nothing to compare against. diff --git a/scripts/verify.py b/scripts/verify.py index 437077555..29c64e108 100644 --- a/scripts/verify.py +++ b/scripts/verify.py @@ -37,6 +37,7 @@ import slots sys.path.insert(0, os.path.dirname(__file__)) from hashing import parse_md5_list +from alternatives import set_keys, settle_sets, slot_set_keys from common import ( _foreign_tree, _owner_tokens, @@ -534,6 +535,9 @@ def find_undeclared_files( config, profiles, relevant, db, declared_destinations(config) ) launcher_candidates: list[tuple[str, bool, dict, dict]] = [] + # Each grouped requirement met, as (set keys, entry, fills, (reported + # entry, the entry it is a claim on)): settled once every core is read. + set_records: list[tuple[tuple, dict, bool, tuple[dict, dict | None] | None]] = [] undeclared = [] seen_files: set[tuple] = set() # Key -> the entry reported for it. A later profile reading the same @@ -606,6 +610,9 @@ def find_undeclared_files( ) if verdict == "settled": seen_files.add(seen_key) + # Answered by the platform's own declaration, which its + # report covers: the set it belongs to is spoken for. + set_records.append((set_keys((emu_name, is_standalone), f), f, True, None)) continue if verdict == "skip": continue @@ -761,12 +768,16 @@ def find_undeclared_files( entry["members"] = members if launcher_layer and not save_root: launcher_candidates.append((emu_name, is_standalone, f, entry)) + keys = set_keys((emu_name, is_standalone), f) if duplicate: first.setdefault("other_claims", []).append(entry) + set_records.append((keys, f, in_repo, (entry, first))) continue reported[seen_key] = entry undeclared.append(entry) + set_records.append((keys, f, in_repo, (entry, None))) + settled: set[int] = set() if launcher_layer: added, settled = launcher_layer.settle( launcher_candidates, lambda report: held_record(report, db) @@ -774,12 +785,63 @@ def find_undeclared_files( undeclared = [u for u in undeclared if id(u) not in settled] undeclared.extend(added) + undeclared = _settle_core_sets(undeclared, set_records, settled) + for entry in sorted(archive_entries.values(), key=lambda e: e["name"]): undeclared.append(entry) return undeclared +def _settle_core_sets( + undeclared: list[dict], + records: list[tuple[tuple, dict, bool, tuple[dict, dict | None] | None]], + launcher_settled: set[int], +) -> list[dict]: + """The core complement with each set of alternatives settled. + + A member the collection lacks is dropped when another member is held, + declared by the platform or placed by its launcher, and a set nobody + holds keeps one entry, its first member, naming the others. A claim + another profile shares stays: its own core may need that very file. + """ + grouped = [r for r in records if r[0]] + if not grouped: + return undeclared + excused, standing = settle_sets([ + (keys, f, fills or (ref is not None and id(ref[0]) in launcher_settled)) + for keys, f, fills, ref in grouped + ]) + dropped: set[int] = set() + for position, (_keys, _f, _fills, ref) in enumerate(grouped): + if ref is None: + continue + entry, host = ref + if position in standing: + entry["alternatives"] = standing[position] + if position not in excused: + continue + if host is not None: + host["other_claims"] = [c for c in host.get("other_claims", []) if c is not entry] + if not host["other_claims"]: + del host["other_claims"] + else: + dropped.add(id(entry)) + out: list[dict] = [] + for entry in undeclared: + if id(entry) not in dropped: + out.append(entry) + continue + # Another core reading the same requirement still needs it. + claims = entry.get("other_claims") or [] + if claims: + promoted = dict(claims[0]) + if claims[1:]: + promoted["other_claims"] = claims[1:] + out.append(promoted) + return out + + def find_exclusion_notes( config: dict, emulators_dir: str, @@ -1802,6 +1864,8 @@ def _print_undeclared_entry( name_label = f"{name} ({arc_count} file{'s' if arc_count != 1 else ''})" else: name_label = name + if u.get("alternatives") and not u.get("in_repo"): + name_label += f" (or {', '.join(u['alternatives'])})" print(f" {prefix}: {u['emulator']} needs {name_label}") checks = u.get("checks", []) if checks: @@ -2171,6 +2235,9 @@ def verify_emulator( return local_path, status details = [] + # Members of a set another member fills, or that the set's first member + # stands for: not missing, and out of every count. + set_members: list[dict] = [] file_status: dict[str, str] = {} file_severity: dict[str, str] = {} dest_to_name: dict[str, str] = {} @@ -2397,7 +2464,9 @@ def verify_emulator( # profile on one destination are the releases the code accepts # there (a shareware and a retail HOG, each GRP in a size table): # holding one of them fills the slot, and the others are not gaps. - emu_results: dict[str, list[tuple[dict, bool, bool]]] = {} + emu_results: dict[str, list[tuple[dict, bool, bool, dict]]] = {} + placed_results: list[tuple[str, dict, bool, bool, list[dict]]] = [] + fallback_reason = "declared hash contradicted by the local file" for file_entry in files: # Skip archive members (verified as archive units above) @@ -2497,13 +2566,13 @@ def verify_emulator( # The slot the emulator pack places this entry in: standalone_path # under --standalone, the sanitised path. dest = _resolve_destination(file_entry, standalone) or name - emu_results.setdefault(dest, []).append((result, required, hle)) + emu_results.setdefault(dest, []).append((result, required, hle, file_entry)) for dest, alternatives in emu_results.items(): # What the emulator pack places here: an accepted file, else a # file whose hash the profile contradicts. A refused copy is held # but never packed. - fallback_reason = "declared hash contradicted by the local file" + slot_entries = [a[3] for a in alternatives] if len(alternatives) > 1: # Reduced in the order generate_emulator_pack chooses, so the # destination's status is the one the pack reports: a slot it @@ -2556,27 +2625,49 @@ def verify_emulator( elif not block and packed is not None: occupy(dest, packed, emu_name) packed_dests.add(dest) - for result, required, hle in alternatives: + for result, required, hle, file_entry in alternatives: if block_reason and result["status"] != Status.MISSING: result["status"] = Status.MISSING result["reason"] = block_reason result["destination"] = dest - details.append(result) + placed_results.append((dest, result, required, hle, slot_entries)) - # Aggregate by destination (path if available, else name) - dest_to_name[dest] = result["name"] - cur = result["status"] - prev = file_status.get(dest) - if prev is None or _STATUS_ORDER.get(cur, 0) > _STATUS_ORDER.get( - prev, 0 - ): - file_status[dest] = cur - sev = _emulator_severity(cur, required, hle) - prev_sev = file_severity.get(dest) - if prev_sev is None or _SEVERITY_ORDER.get( - sev, 0 - ) > _SEVERITY_ORDER.get(prev_sev, 0): - file_severity[dest] = sev + # A set the code takes one member of is one need: once a member is + # in the pack the others are not missing, and a set nobody holds is + # one gap under its first member. Settled per profile, before the + # destinations of several profiles are merged. + excused, standing = settle_sets([ + ( + slot_set_keys(emu_name, entries), + entries[0], + result["status"] == Status.OK + or (result["status"] == Status.UNTESTED + and result.get("reason") == fallback_reason), + ) + for _dest, result, _required, _hle, entries in placed_results + ]) + for position, (dest, result, required, hle, _fe) in enumerate(placed_results): + if position in excused and result["status"] == Status.MISSING: + set_members.append(result) + continue + if position in standing: + result["alternatives"] = standing[position] + details.append(result) + + # Aggregate by destination (path if available, else name) + dest_to_name[dest] = result["name"] + cur = result["status"] + prev = file_status.get(dest) + if prev is None or _STATUS_ORDER.get(cur, 0) > _STATUS_ORDER.get( + prev, 0 + ): + file_status[dest] = cur + sev = _emulator_severity(cur, required, hle) + prev_sev = file_severity.get(dest) + if prev_sev is None or _SEVERITY_ORDER.get( + sev, 0 + ) > _SEVERITY_ORDER.get(prev_sev, 0): + file_severity[dest] = sev occupy_trees(emu_name, profile) @@ -2610,6 +2701,7 @@ def verify_emulator( "severity_counts": counts, "status_counts": status_counts, "details": details, + "set_members": set_members, "data_dir_notices": sorted(set(data_dir_notices)), "ground_truth_coverage": { "with_validation": with_validation, @@ -2715,7 +2807,9 @@ def print_emulator_result(result: dict, verbose: bool = False) -> None: seen.add(d["name"]) req = "required" if d.get("required", True) else "optional" hle = ", HLE available" if d.get("hle_fallback") else "" - print(f" MISSING ({req}{hle}): {d['name']}") + others = d.get("alternatives") + either = f" (or {', '.join(others)})" if others else "" + print(f" MISSING ({req}{hle}): {d['name']}{either}") gt = d.get("ground_truth", []) if gt: if verbose: diff --git a/tests/test_e2e.py b/tests/test_e2e.py index ada7bd5ce..6d727698e 100644 --- a/tests/test_e2e.py +++ b/tests/test_e2e.py @@ -6765,5 +6765,219 @@ class StandaloneModeRequiresAStandaloneBuild(unittest.TestCase): self.assertFalse(runs_standalone("dolphin", dual, {"pcsx2"})) + +class OneOfASearchList(unittest.TestCase): + """Entries a core takes the first of are one need (``group:``). + + Yabause tries saturn_bios.bin, sega_101.bin, mpr-17933.bin and loads the + first it finds. Written as required on the first name only, verify and + the gap report called saturn_bios.bin a missing required file while + sega_101.bin was held, and a set held through an optional-looking name + passed for the wrong reason. Synthetic: set ``first`` holds only its + second member, set ``none`` holds nothing. + """ + + def setUp(self): + from common import _emulator_profiles_cache + + _emulator_profiles_cache.clear() + repo = Path(__file__).resolve().parent.parent + (repo / "tmp").mkdir(exist_ok=True) + self.root = Path(tempfile.mkdtemp(dir=repo / "tmp")) + self.addCleanup(shutil.rmtree, self.root, True) + previous = os.getcwd() + os.chdir(self.root) + self.addCleanup(os.chdir, previous) + self.db = {"files": {}, "indexes": {"by_md5": {}, "by_name": {}, + "by_crc32": {}, "by_path_suffix": {}}} + for name in ("second.bin", "plain.bin"): + data = name.encode() * 8 + rel = f"bios/Test/Console/{name}" + Path(rel).parent.mkdir(parents=True, exist_ok=True) + Path(rel).write_bytes(data) + sha1 = hashlib.sha1(data).hexdigest() + self.db["files"][sha1] = {"path": rel, "name": name, "size": len(data), + "md5": hashlib.md5(data).hexdigest(), "crc32": ""} + self.db["indexes"]["by_name"][name] = [sha1] + Path("emulators").mkdir() + Path("platforms").mkdir() + + def member(name: str, group: str, rank: int) -> dict: + return {"name": name, "system": "console-a", "group": group, + "priority": rank, "required": True} + + self.profile = { + "emulator": "Seq", "type": "libretro", "cores": ["seq"], + "systems": ["console-a"], + "files": [ + member("first.bin", "first", 1), + member("second.bin", "first", 2), + member("third.bin", "first", 3), + member("none_a.bin", "none", 1), + member("none_b.bin", "none", 2), + {"name": "plain.bin", "system": "console-a", "required": True}, + ], + } + Path("emulators/seq.yml").write_text(yaml.safe_dump(self.profile)) + self.config = { + "platform": "Seqs", "verification_mode": "existence", + "base_destination": "system", "cores": ["seq"], + "systems": {"console-a": {"files": []}}, + } + + def _verify(self) -> dict: + return verify_emulator(["seq"], "emulators", self.db, platforms_dir="platforms") + + def test_one_member_held_leaves_no_missing_report_for_its_set(self): + result = self._verify() + reported = {d["name"]: d["status"] for d in result["details"]} + self.assertEqual(reported["second.bin"], "ok") + self.assertNotIn("first.bin", reported) + self.assertNotIn("third.bin", reported) + self.assertEqual( + sorted(d["name"] for d in result["set_members"]), + ["first.bin", "none_b.bin", "third.bin"], + ) + + def test_a_set_none_holds_is_one_gap(self): + result = self._verify() + missing = [d for d in result["details"] if d["status"] == "missing"] + self.assertEqual([d["name"] for d in missing], ["none_a.bin"]) + self.assertEqual(missing[0]["alternatives"], ["none_b.bin"]) + # Required and missing, judged as the core judges: one warning. + self.assertEqual(result["severity_counts"][Severity.WARNING], 1) + self.assertEqual(result["severity_counts"][Severity.INFO], 0) + self.assertEqual(result["status_counts"].get("missing"), 1) + self.assertEqual(result["total_files"], 3) + + def test_a_required_set_held_through_a_later_member_is_satisfied(self): + """second.bin used to read required: false beside a required + first.bin: the set is required and held, so nothing is critical + for it, and the strict exit names only the set nobody holds.""" + from verify import _exit_if_strict_emulator + + result = self._verify() + critical = [d["name"] for d in result["details"] + if d["status"] == "missing" and d["required"]] + self.assertEqual(critical, ["none_a.bin"]) + with self.assertRaises(SystemExit), contextlib.redirect_stderr(io.StringIO()) as err: + _exit_if_strict_emulator(True, result) + self.assertIn("1 required file(s) missing: none_a.bin", err.getvalue()) + + def test_pack_and_verify_agree(self): + from generate_pack import generate_emulator_pack + + result = self._verify() + out = io.StringIO() + with contextlib.redirect_stdout(out): + zip_path = generate_emulator_pack( + ["seq"], "emulators", self.db, "bios", "out", offline=True, + platforms_dir="platforms", + ) + with zipfile.ZipFile(zip_path) as zf: + packed = sorted(n for n in zf.namelist() if n != "README.txt") + self.assertEqual(packed, ["plain.bin", "second.bin"]) + self.assertEqual(result["severity_counts"]["ok"], len(packed)) + lines = out.getvalue().splitlines() + self.assertEqual( + [line.split(": ", 1)[1] for line in lines if line.strip().startswith("MISSING:")], + ["none_a.bin"], + ) + self.assertIn("2 files packed, 1 missing", out.getvalue()) + + def test_core_complement_reports_one_gap_per_set(self): + undeclared = find_undeclared_files( + self.config, "emulators", self.db, {"seq": self.profile} + ) + by_name = {u["name"]: u for u in undeclared} + self.assertEqual(sorted(by_name), ["none_a.bin", "plain.bin", "second.bin"]) + self.assertTrue(by_name["second.bin"]["in_repo"]) + self.assertFalse(by_name["none_a.bin"]["in_repo"]) + self.assertEqual(by_name["none_a.bin"]["alternatives"], ["none_b.bin"]) + + def test_a_platform_declaration_speaks_for_the_set(self): + """The platform declares first.bin and reports it in its own check: + the core's other names are not gaps on top of it.""" + config = {**self.config, "systems": {"console-a": {"files": [ + {"name": "first.bin", "destination": "first.bin", "required": True}, + {"name": "none_b.bin", "destination": "none_b.bin", "required": True}, + ]}}} + undeclared = find_undeclared_files( + config, "emulators", self.db, {"seq": self.profile} + ) + self.assertEqual(sorted(u["name"] for u in undeclared), ["plain.bin", "second.bin"]) + + def test_gap_report_counts_the_set_once(self): + import cross_reference + + index = { + "by_name": self.db["indexes"]["by_name"], + "by_name_lower": {k.lower(): k for k in self.db["indexes"]["by_name"]}, + "by_path_suffix": {}, "by_md5": {}, "by_sha256": {}, "by_crc32": {}, + "data_names": set(), "all_declared": set(), "db_files": self.db["files"], + } + report: dict = {} + cross_reference._cross_reference_profile("seq", self.profile, {}, report, index) + gaps = {g["name"]: g for g in report["seq"]["gap_details"]} + self.assertEqual(sorted(gaps), ["none_a.bin", "plain.bin", "second.bin"]) + self.assertEqual(gaps["none_a.bin"]["alternatives"], ["none_b.bin"]) + self.assertEqual(report["seq"]["set_aside"]["other_versions"], 3) + + def test_truth_names_no_member_required_on_its_own(self): + from truth import _fold_into, _new_truth_entry + + entry = _new_truth_entry(self.profile["files"][0], "seq", None) + self.assertNotIn("required", entry) + self.assertEqual(entry["_required_by"], set()) + self.assertTrue(entry["one_of_required"]) + _fold_into(entry, {"name": "first.bin", "required": True}, "other") + self.assertTrue(entry["required"]) + self.assertEqual(entry["_required_by"], {"other"}) + + def test_ranks_order_one_set_and_never_two(self): + """px68k-sdl ranks its IPL list and its font list each from 1: + --one-per-slot keeps the first of each, not one file of the two.""" + from slot import build_slot_index, resolve_slot_drops + + profile = {"type": "standalone", "files": [ + {"name": "ipl_a.dat", "group": "ipl", "priority": 1}, + {"name": "ipl_b.dat", "group": "ipl", "priority": 2}, + {"name": "font_a.dat", "group": "font", "priority": 1}, + {"name": "font_b.dat", "group": "font", "priority": 2}, + ]} + index = build_slot_index({"x68": profile}) + groups = {"sharp-x68000": [(n, n) for n in + ("ipl_a.dat", "ipl_b.dat", "font_a.dat", "font_b.dat")]} + drops, undecidable = resolve_slot_drops(groups, index) + self.assertEqual(drops, {"ipl_b.dat", "font_b.dat"}) + self.assertEqual(undecidable, []) + + def test_an_archive_needs_one_member_of_a_set(self): + import zlib + + from common import holds_required_members + + path = self.root / "set.zip" + with zipfile.ZipFile(path, "w") as zf: + zf.writestr("boot_b.ic94", b"B") + zf.writestr("eeprom.ic1", b"E") + crc = {n: f"{zlib.crc32(d):08x}" for n, d in (("a", b"A"), ("b", b"B"), ("e", b"E"))} + members = [ + {"crc32": crc["a"], "required": True, "group": "boot"}, + {"crc32": crc["b"], "required": True, "group": "boot"}, + {"crc32": crc["e"], "required": True}, + ] + self.assertTrue(holds_required_members(str(path), members)) + self.assertFalse(holds_required_members(str(path), members[:1] + members[2:])) + + def test_schema_refuses_a_set_split_on_required(self): + from validate_schemas import _group_requirement_split, ROOT + + path = ROOT / "emulators" / "seq.yml" + split = {"files": [{"name": "a", "group": "g", "required": True}, + {"name": "b", "group": "g", "required": False}]} + self.assertEqual(len(_group_requirement_split(path, split)), 1) + self.assertEqual(_group_requirement_split(path, {"files": self.profile["files"]}), []) + if __name__ == "__main__": unittest.main() diff --git a/wiki/profiling.md b/wiki/profiling.md index feb7414ce..0fa2b6dc3 100644 --- a/wiki/profiling.md +++ b/wiki/profiling.md @@ -603,7 +603,7 @@ look. | Field | Description | |-------|-------------| | `name` | filename as the core expects it | -| `required` | true if the core needs this file to function | +| `required` | true if the core needs this file to function. On a member of a `group`, the requirement of the set | | `system` | system ID this file belongs to (for multi-system profiles) | | `systems` | the systems of the profile this file serves, when it serves several but not all (a MAME device set the machines of two systems load). Never written with `system` | | `archive` | ROM set the file lives inside, for arcade entries (e.g. `neogeo.zip`) | @@ -621,7 +621,8 @@ look. | `region` | always a list of territory slugs from the schema enum (`[north-america]`, `[japan, asia-ntsc]`). The region the code selects the file **for**, never the region of the dump. See step 3b | | `region_check` | true if the core refuses to boot a game whose region does not match the BIOS | | `fast_boot` | BIOS generation label the core uses to decide whether fast boot is available | -| `priority` | preference order when several BIOS satisfy the same slot, **lowest wins**. DuckStation's `FindBIOSImageInDirectory` keeps the image whose priority is lower, and its table de-prioritizes by raising the number: the buggy launch console sits at 50 and PS2 images at 100, while `scph5501` sits at 5. A core that walks an ordered search list, as PicoDrive does with `biosfiles_us/eu/jp`, maps onto the same field as 1, 2, 3. Only set it from the source. `--one-per-slot` reads it | +| `priority` | preference order when several BIOS satisfy the same slot, **lowest wins**. DuckStation's `FindBIOSImageInDirectory` keeps the image whose priority is lower, and its table de-prioritizes by raising the number: the buggy launch console sits at 50 and PS2 images at 100, while `scph5501` sits at 5. A core that walks an ordered search list, as PicoDrive does with `biosfiles_us/eu/jp`, maps onto the same field as 1, 2, 3. Only set it from the source. `--one-per-slot` reads it, comparing the members of one `group` | +| `group` | entries the code takes one of: it loads the first it finds (Yabause tries `saturn_bios.bin`, `sega_101.bin`, `mpr-17933.bin`), and holding any member fills the need. Verify, the gap report and the packs report a set nobody holds as one gap under its first member, and no member of a held set as missing. `required` is the same on every member, the order stays in `priority`. A list puts a file in several sets. Where the code keeps a list per region, each list is its own group (PicoDrive's `mega-cd-us`, `mega-cd-eu`, `mega-cd-jp`) | | `embedded` | true if the data is compiled into the binary and the external file is optional | | `bundled` | true if the emulator's own release installs the file where it reads it, for the build `mode` names (all builds when `mode` is absent). Packs and the installer leave it out: the user already has it. A file the standalone release ships but the libretro core does not (PPSSPP's assets, Dolphin's Sys) is bundled with `mode: standalone` only | | `not_bundled_on` | platforms (registry keys) whose package leaves out a file the release bundles: their packs bring it (Batocera installs hcl without its data) | From 1e12d9fe38480c78b9a60f7425a80e9837943a94 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Mon, 9 Nov 2026 17:40:00 +0100 Subject: [PATCH 2/6] feat: group the first-found BIOS lists Each list read at the profile's pin, every member required: true where the code needs one of the set, the order left in priority. - yabause, yabasanshiro, kronos: libretro.c tries saturn_bios.bin, sega_101.bin, mpr-17933.bin (yabause 1035-1049, yabasanshiro 1048-1071, kronos 1619-1638 after kronos/saturn_bios.bin). kronos also reads saturn_bios.bin at the system root, now its own entry. yabasanshiro refuses the game with no file even under forced HLE (1064-1071), so its hle_fallback is gone. - yabadabadoo: bios_for_region (libretro.c:262-284) takes any of the three in an order the disc region picks. One group, no priority: region keeps that order, no single rank would be true. - ymir libretro: kBiosFilenames (libretro.cpp:305-309), region first, else the first found, no game without one (1231-1236). - duckstation: FindBIOSImageInDirectory (bios.cpp:397-450) keeps any image of the right size, a region mismatch only warns: one set. - picodrive: find_bios walks one list per disc region (libretro.c:1272-1336), three groups. - px68k: BIOSFILE then cgrom.dat or cgrom.tmp (libretro.c:984-1032). - px68k-sdl: BIOSFILE and FONTFILE lists (winx68k.cpp:277-331). - numero: ti83se, ti83plus, ti83 (libretronew.cpp:575-594), no ROM leaves a blank screen. - freechaf: sl90025 then sl31253, HLE without either (libretro.c:186-199), optional set. - pcsx1: scph1001, scph5501, scph7001 then any scph* (libretro.c:1229), HLE without, optional set. - odin: three boot ROMs inside hikaru.zip. --- emulators/duckstation.yml | 309 +++++++++++++++++++++++++------------ emulators/freechaf.yml | 2 + emulators/kronos.yml | 23 ++- emulators/numero.yml | 9 +- emulators/odin.yml | 9 +- emulators/pcsx1.yml | 3 + emulators/picodrive.yml | 30 ++-- emulators/px68k-sdl.yml | 20 ++- emulators/px68k.yml | 16 +- emulators/yabadabadoo.yml | 7 +- emulators/yabasanshiro.yml | 16 +- emulators/yabause.yml | 7 +- emulators/ymir.yml | 5 +- 13 files changed, 311 insertions(+), 145 deletions(-) diff --git a/emulators/duckstation.yml b/emulators/duckstation.yml index b457e9363..b4c67cd73 100644 --- a/emulators/duckstation.yml +++ b/emulators/duckstation.yml @@ -81,7 +81,8 @@ files: standalone_path: "bios/scph1000.bin" description: "SCPH-1000, DTL-H1000 (v1.0)" region: [japan] - required: false + group: ps-bios + required: true md5: "239665b1a3dade1b5a52c06338011044" region_check: true fast_boot: Type1 @@ -92,7 +93,8 @@ files: standalone_path: "bios/scph1001.bin" description: "SCPH-1001, 5003, DTL-H1201, H3001 (v2.2 12-04-95 A)" region: [north-america, latin-america] - required: false + group: ps-bios + required: true md5: "924e392ed05558ffdb115408c263dccf" region_check: false fast_boot: Type1 @@ -103,7 +105,8 @@ files: standalone_path: "bios/scph1002a.bin" description: "SCPH-1002, DTL-H1002 (v2.0 05-10-95 E)" region: [europe, oceania] - required: false + group: ps-bios + required: true md5: "54847e693405ffeb0359c6287434cbef" region_check: false fast_boot: Type1 @@ -114,7 +117,8 @@ files: standalone_path: "bios/scph1002b.bin" description: "SCPH-1002, DTL-H1102 (v2.1 07-17-95 E)" region: [europe, oceania] - required: false + group: ps-bios + required: true md5: "417b34706319da7cf001e76e40136c23" region_check: false fast_boot: Type1 @@ -125,7 +129,8 @@ files: standalone_path: "bios/scph1002c.bin" description: "SCPH-1002, DTL-H1202, H3002 (v2.2 12-04-95 E)" region: [europe, oceania] - required: false + group: ps-bios + required: true md5: "e2110b8a2b97a8e0b857a45d32f7e187" region_check: false fast_boot: Type1 @@ -136,7 +141,8 @@ files: standalone_path: "bios/dtlh1100.bin" description: "DTL-H1100 (v2.2 03-06-96 D)" region: [japan] - required: false + group: ps-bios + required: true md5: "ca5cfc321f916756e3f0effbfaeba13b" region_check: true fast_boot: Type1 @@ -147,7 +153,8 @@ files: standalone_path: "bios/scph3000.bin" description: "SCPH-3000, DTL-H1000H (v1.1 01-22-95)" region: [japan] - required: false + group: ps-bios + required: true md5: "849515939161e62f6b866f6853006780" region_check: true fast_boot: Type1 @@ -158,7 +165,8 @@ files: standalone_path: "bios/scph1001_v20.bin" description: "SCPH-1001, DTL-H1001 (v2.0 05-07-95 A)" region: [north-america, latin-america] - required: false + group: ps-bios + required: true md5: "dc2b9bf8da62ec93e868cfd29f0d067d" region_check: false fast_boot: Type1 @@ -169,7 +177,8 @@ files: standalone_path: "bios/scph3500.bin" description: "SCPH-3500 (v2.1 07-17-95 J)" region: [japan] - required: false + group: ps-bios + required: true md5: "cba733ceeff5aef5c32254f1d617fa62" region_check: true fast_boot: Type1 @@ -180,7 +189,8 @@ files: standalone_path: "bios/scph1001_v21.bin" description: "SCPH-1001, DTL-H1101 (v2.1 07-17-95 A)" region: [north-america, latin-america] - required: false + group: ps-bios + required: true md5: "da27e8b6dab242d8f91a9b25d80c63b8" region_check: false fast_boot: Type1 @@ -191,7 +201,8 @@ files: standalone_path: "bios/scph5000.bin" description: "SCPH-5000, DTL-H1200, H3000 (v2.2 12-04-95 J)" region: [japan] - required: false + group: ps-bios + required: true md5: "57a06303dfa9cf9351222dfcbb4a29d9" region_check: true fast_boot: Type1 @@ -202,6 +213,7 @@ files: standalone_path: "bios/scph5500.bin" description: "SCPH-5500 (v3.0 09-09-96 J)" region: [japan] + group: ps-bios required: true md5: "8dd7d5296a650fac7319bce665a6a53c" region_check: true @@ -213,6 +225,7 @@ files: standalone_path: "bios/scph5501.bin" description: "SCPH-5501, 5503, 7003 (v3.0 11-18-96 A)" region: [north-america, latin-america] + group: ps-bios required: true md5: "490f666e1afb15b7362b406ed1cea246" region_check: false @@ -224,6 +237,7 @@ files: standalone_path: "bios/scph5502.bin" description: "SCPH-5502, 5552 (v3.0 01-06-97 E)" region: [europe, oceania] + group: ps-bios required: true md5: "32736f17079d0b2b7024407c39bd3050" region_check: false @@ -235,7 +249,8 @@ files: standalone_path: "bios/scph7000.bin" description: "SCPH-7000, 7500, 9000 (v4.0 08-18-97 J)" region: [japan] - required: false + group: ps-bios + required: true md5: "8e4c14f567745eff2f0408c8129f72a6" region_check: true fast_boot: Type1 @@ -246,7 +261,8 @@ files: standalone_path: "bios/scph7000w.bin" description: "SCPH-7000W (v4.1 11-14-97 A)" region: [japan] - required: false + group: ps-bios + required: true md5: "b84be139db3ee6cbd075630aa20a6553" region_check: false fast_boot: Type1 @@ -257,7 +273,8 @@ files: standalone_path: "bios/scph7001.bin" description: "SCPH-7001, 7501, 7503, 9001, 9003, 9903 (v4.1 12-16-97 A)" region: [north-america, latin-america] - required: false + group: ps-bios + required: true md5: "1e68c231d0896b7eadcad1d7d8e76129" region_check: false fast_boot: Type1 @@ -268,7 +285,8 @@ files: standalone_path: "bios/scph7002.bin" description: "SCPH-7002, 7502, 9002 (v4.1 12-16-97 E)" region: [europe, oceania] - required: false + group: ps-bios + required: true md5: "b9d9a0286c33dc6b7237bb13cd46fdee" region_check: false fast_boot: Type1 @@ -279,7 +297,8 @@ files: standalone_path: "bios/scph100.bin" description: "SCPH-100 (v4.3 03-11-00 J)" region: [japan] - required: false + group: ps-bios + required: true md5: "8abc1b549a4a80954addc48ef02c4521" region_check: true fast_boot: Type1 @@ -290,7 +309,8 @@ files: standalone_path: "bios/scph101_v44.bin" description: "SCPH-101 (v4.4 03-24-00 A)" region: [north-america, latin-america] - required: false + group: ps-bios + required: true md5: "9a09ab7e49b422c007e6d54d7c49b965" region_check: false fast_boot: Type1 @@ -301,7 +321,8 @@ files: standalone_path: "bios/scph101_v45.bin" description: "SCPH-101 (v4.5 05-25-00 A)" region: [north-america, latin-america] - required: false + group: ps-bios + required: true md5: "6e3735ff4c7dc899ee98981385f6f3d0" region_check: false fast_boot: Type1 @@ -312,7 +333,8 @@ files: standalone_path: "bios/scph102_v44.bin" description: "SCPH-102 (v4.4 03-24-00 E)" region: [europe, oceania] - required: false + group: ps-bios + required: true md5: "b10f5e0e3d9eb60e5159690680b1e774" region_check: true fast_boot: Type1 @@ -323,7 +345,8 @@ files: standalone_path: "bios/scph102_v45.bin" description: "SCPH-102 (v4.5 05-25-00 E)" region: [europe, oceania] - required: false + group: ps-bios + required: true md5: "de93caec13d1a141a40a79f5c86168d6" region_check: true fast_boot: Type1 @@ -334,7 +357,8 @@ files: standalone_path: "bios/scph1000r.bin" description: "SCPH-1000R (v4.5 05-25-00 J)" region: [japan] - required: false + group: ps-bios + required: true md5: "476d68a94ccec3b9c8303bbd1daf2810" region_check: true fast_boot: Type1 @@ -349,7 +373,8 @@ files: standalone_path: "bios/ps2_scph18000.bin" description: "PS2, SCPH-18000 (v5.0 10-27-00 J)" region: [japan] - required: false + group: ps-bios + required: true md5: "d8f485717a5237285e4d7c5f881b7f32" region_check: true fast_boot: Type2 @@ -360,7 +385,8 @@ files: standalone_path: "bios/ps2_scph30003.bin" description: "PS2, SCPH-30003 (v5.0 09-02-00 E)" region: [europe, oceania] - required: false + group: ps-bios + required: true md5: "71f50ef4f4e17c163c78908e16244f7d" region_check: true fast_boot: Type2 @@ -371,7 +397,8 @@ files: standalone_path: "bios/ps2_dtlh10000_0117.bin" description: "PS2, DTL-H10000 (v5.0 01/17/00 T)" region: [world] - required: false + group: ps-bios + required: true md5: "32f2e4d5ff5ee11072a6bc45530f5765" region_check: true fast_boot: Type2 @@ -382,7 +409,8 @@ files: standalone_path: "bios/ps2_scph10000_0117.bin" description: "PS2, SCPH-10000 (v5.0 01/17/00 T)" region: [world] - required: false + group: ps-bios + required: true md5: "acf4730ceb38ac9d8c7d8e21f2614600" region_check: true fast_boot: Type2 @@ -393,7 +421,8 @@ files: standalone_path: "bios/ps2_dtlh10000_0217.bin" description: "PS2, DTL-H10000 (v5.0 02/17/00 T)" region: [world] - required: false + group: ps-bios + required: true md5: "acf9968c8f596d2b15f42272082513d1" region_check: true fast_boot: Type2 @@ -404,7 +433,8 @@ files: standalone_path: "bios/ps2_scph10000_15000.bin" description: "PS2, SCPH-10000/SCPH-15000 (v5.0 02/17/00 T)" region: [world] - required: false + group: ps-bios + required: true md5: "b1459d7446c69e3e97e6ace3ae23dd1c" region_check: true fast_boot: Type2 @@ -415,7 +445,8 @@ files: standalone_path: "bios/ps2_dtlh10000_0224.bin" description: "PS2, DTL-H10000 (v5.0 02/24/00 T)" region: [world] - required: false + group: ps-bios + required: true md5: "d3f1853a16c2ec18f3cd1ae655213308" region_check: true fast_boot: Type2 @@ -426,7 +457,8 @@ files: standalone_path: "bios/ps2_dtlh30001.bin" description: "PS2, DTL-H30001 (v5.0 07/27/00 A)" region: [north-america, latin-america] - required: false + group: ps-bios + required: true md5: "63e6fd9b3c72e0d7b920e80cf76645cd" region_check: true fast_boot: Type2 @@ -437,7 +469,8 @@ files: standalone_path: "bios/ps2_scph30001_0727.bin" description: "PS2, SCPH-30001 (v5.0 07/27/00 A)" region: [north-america, latin-america] - required: false + group: ps-bios + required: true md5: "a20c97c02210f16678ca3010127caf36" region_check: true fast_boot: Type2 @@ -448,7 +481,8 @@ files: standalone_path: "bios/ps2_scph30001_0902.bin" description: "PS2, SCPH-30001 (v5.0 09/02/00 A)" region: [north-america, latin-america] - required: false + group: ps-bios + required: true md5: "8db2fbbac7413bf3e7154c1e0715e565" region_check: true fast_boot: Type2 @@ -459,7 +493,8 @@ files: standalone_path: "bios/ps2_dtlh30002.bin" description: "PS2, DTL-H30002 (v5.0 09/02/00 E)" region: [europe, oceania] - required: false + group: ps-bios + required: true md5: "91c87cb2f2eb6ce529a2360f80ce2457" region_check: true fast_boot: Type2 @@ -470,7 +505,8 @@ files: standalone_path: "bios/ps2_dtlh30102_0902.bin" description: "PS2, DTL-H30102 (v5.0 09/02/00 E)" region: [europe, oceania] - required: false + group: ps-bios + required: true md5: "3016b3dd42148a67e2c048595ca4d7ce" region_check: true fast_boot: Type2 @@ -481,7 +517,8 @@ files: standalone_path: "bios/ps2_scph30002_0902.bin" description: "PS2, SCPH-30002/SCPH-30003/SCPH-30004 (v5.0 09/02/00 E)" region: [europe, oceania] - required: false + group: ps-bios + required: true md5: "b7fa11e87d51752a98b38e3e691cbf17" region_check: true fast_boot: Type2 @@ -492,7 +529,8 @@ files: standalone_path: "bios/ps2_scph18000_gh003.bin" description: "PS2, SCPH-18000 (GH-003) (v5.0 10/27/00 J)" region: [japan] - required: false + group: ps-bios + required: true md5: "f63bc530bd7ad7c026fcd6f7bd0d9525" region_check: true fast_boot: Type2 @@ -503,7 +541,8 @@ files: standalone_path: "bios/ps2_scph18000_gh008.bin" description: "PS2, SCPH-18000 (GH-008) (v5.0 10/27/00 J)" region: [japan] - required: false + group: ps-bios + required: true md5: "cee06bd68c333fc5768244eae77e4495" region_check: true fast_boot: Type2 @@ -514,7 +553,8 @@ files: standalone_path: "bios/ps2_dtlh30101.bin" description: "PS2, DTL-H30101 (v5.0 12/28/00 A)" region: [north-america, latin-america] - required: false + group: ps-bios + required: true md5: "0bf988e9c7aaa4c051805b0fa6eb3387" region_check: true fast_boot: Type2 @@ -525,7 +565,8 @@ files: standalone_path: "bios/ps2_scph30001_35001.bin" description: "PS2, SCPH-30001/SCPH-35001 (v5.0 12/28/00 A)" region: [north-america, latin-america] - required: false + group: ps-bios + required: true md5: "8accc3c49ac45f5ae2c5db0adc854633" region_check: true fast_boot: Type2 @@ -536,7 +577,8 @@ files: standalone_path: "bios/ps2_dtlh30102_1228.bin" description: "PS2, DTL-H30102 (v5.0 12/28/00 E)" region: [europe, oceania] - required: false + group: ps-bios + required: true md5: "6f9a6feb749f0533aaae2cc45090b0ed" region_check: true fast_boot: Type2 @@ -547,7 +589,8 @@ files: standalone_path: "bios/ps2_scph30002_1228.bin" description: "PS2, SCPH-30002/SCPH-30003/SCPH-30004/SCHP-35002/SCPH-35003/SCPH-35004 (v5.0 12/28/00 E)" region: [europe, oceania] - required: false + group: ps-bios + required: true md5: "838544f12de9b0abc90811279ee223c8" region_check: true fast_boot: Type2 @@ -558,7 +601,8 @@ files: standalone_path: "bios/ps2_dtlh30000.bin" description: "PS2, DTL-H30000 (v5.0 01/18/01 J)" region: [japan] - required: false + group: ps-bios + required: true md5: "bb6bbc850458fff08af30e969ffd0175" region_check: true fast_boot: Type2 @@ -569,7 +613,8 @@ files: standalone_path: "bios/ps2_scph30000_35000.bin" description: "PS2, SCPH-30000/SCPH-35000 (v5.0 01/18/01 J)" region: [japan] - required: false + group: ps-bios + required: true md5: "815ac991d8bc3b364696bead3457de7d" region_check: true fast_boot: Type2 @@ -580,7 +625,8 @@ files: standalone_path: "bios/ps2_scph30001r_0427.bin" description: "PS2, SCPH-30001R (v5.0 04/27/01 A)" region: [north-america, latin-america] - required: false + group: ps-bios + required: true md5: "b107b5710042abe887c0f6175f6e94bb" region_check: true fast_boot: Type2 @@ -591,7 +637,8 @@ files: standalone_path: "bios/ps2_scph30000_0427.bin" description: "PS2, SCPH-30000 (v5.0 04/27/01 J)" region: [japan] - required: false + group: ps-bios + required: true md5: "ab55cceea548303c22c72570cfd4dd71" region_check: true fast_boot: Type2 @@ -602,7 +649,8 @@ files: standalone_path: "bios/ps2_scph30001r_0704.bin" description: "PS2, SCPH-30001R (v5.0 07/04/01 A)" region: [north-america, latin-america] - required: false + group: ps-bios + required: true md5: "18bcaadb9ff74ed3add26cdf709fff2e" region_check: true fast_boot: Type2 @@ -613,7 +661,8 @@ files: standalone_path: "bios/ps2_scph30002r_0704.bin" description: "PS2, SCPH-30002R/SCPH-30003R/SCPH-30004R (v5.0 07/04/01 E)" region: [europe, oceania] - required: false + group: ps-bios + required: true md5: "491209dd815ceee9de02dbbc408c06d6" region_check: true fast_boot: Type2 @@ -624,7 +673,8 @@ files: standalone_path: "bios/ps2_scph30001r_1004.bin" description: "PS2, SCPH-30001R (v5.0 10/04/01 A)" region: [north-america, latin-america] - required: false + group: ps-bios + required: true md5: "7200a03d51cacc4c14fcdfdbc4898431" region_check: true fast_boot: Type2 @@ -635,7 +685,8 @@ files: standalone_path: "bios/ps2_scph30002r_1004.bin" description: "PS2, SCPH-30002R/SCPH-30003R/SCPH-30004R (v5.0 10/04/01 E)" region: [europe, oceania] - required: false + group: ps-bios + required: true md5: "8359638e857c8bc18c3c18ac17d9cc3c" region_check: true fast_boot: Type2 @@ -646,7 +697,8 @@ files: standalone_path: "bios/ps2_scph30005r.bin" description: "PS2, SCPH-30005R/SCPH-30006R/SCPH-30007R (v5.0 07/30/01 J)" region: [japan] - required: false + group: ps-bios + required: true md5: "352d2ff9b3f68be7e6fa7e6dd8389346" region_check: true fast_boot: Type2 @@ -657,7 +709,8 @@ files: standalone_path: "bios/ps2_scph39001.bin" description: "PS2, SCPH-39001 (v5.0 02/07/02 A)" region: [north-america, latin-america] - required: false + group: ps-bios + required: true md5: "d5ce2c7d119f563ce04bc04dbc3a323e" region_check: true fast_boot: Type2 @@ -668,7 +721,8 @@ files: standalone_path: "bios/ps2_scph39002.bin" description: "PS2, SCPH-39002/SCPH-39003/SCPH-39004 (v5.0 03/19/02 E)" region: [europe, oceania] - required: false + group: ps-bios + required: true md5: "0d2228e6fd4fb639c9c39d077a9ec10c" region_check: true fast_boot: Type2 @@ -679,7 +733,8 @@ files: standalone_path: "bios/ps2_scph37000_39000.bin" description: "PS2, SCPH-37000/SCPH-39000 (v5.0 04/26/02 J)" region: [japan] - required: false + group: ps-bios + required: true md5: "72da56fccb8fcd77bba16d1b6f479914" region_check: true fast_boot: Type2 @@ -690,7 +745,8 @@ files: standalone_path: "bios/ps2_scph39008.bin" description: "PS2, SCPH-39008 (v5.0 04/26/02 E)" region: [europe, oceania] - required: false + group: ps-bios + required: true md5: "5b1f47fbeb277c6be2fccdd6344ff2fd" region_check: true fast_boot: Type2 @@ -701,7 +757,8 @@ files: standalone_path: "bios/ps2_scph39005.bin" description: "PS2, SCPH-39005/SCPH-39006/SCPH-39007 (v5.0 04/26/02 J)" region: [japan] - required: false + group: ps-bios + required: true md5: "315a4003535dfda689752cb25f24785c" region_check: true fast_boot: Type2 @@ -712,7 +769,8 @@ files: standalone_path: "bios/ps2_dtlh50000.bin" description: "PS2, DTL-H50000 (v5.0 02/06/03 J)" region: [japan] - required: false + group: ps-bios + required: true md5: "54ecde087258557e2ddb5c3ddb004028" region_check: true fast_boot: Type2 @@ -723,7 +781,8 @@ files: standalone_path: "bios/ps2_scph50000_55000.bin" description: "PS2, SCPH-50000/SCPH-55000 (v5.0 02/06/03 J)" region: [japan] - required: false + group: ps-bios + required: true md5: "312ad4816c232a9606e56f946bc0678a" region_check: true fast_boot: Type2 @@ -734,7 +793,8 @@ files: standalone_path: "bios/ps2_dtlh50002.bin" description: "PS2, DTL-H50002 (v5.0 02/27/03 E)" region: [europe, oceania] - required: false + group: ps-bios + required: true md5: "666018ffec65c5c7e04796081295c6c7" region_check: true fast_boot: Type2 @@ -745,7 +805,8 @@ files: standalone_path: "bios/ps2_scph50002_0227.bin" description: "PS2, SCPH-50002/SCPH-50003/SCPH-50004 (v5.0 02/27/03 E)" region: [europe, oceania] - required: false + group: ps-bios + required: true md5: "6e69920fa6eef8522a1d688a11e41bc6" region_check: true fast_boot: Type2 @@ -756,7 +817,8 @@ files: standalone_path: "bios/ps2_dtlh50001_0325.bin" description: "PS2, DTL-H50001 (v5.0 03/25/03 A)" region: [north-america, latin-america] - required: false + group: ps-bios + required: true md5: "eb960de68f0c0f7f9fa083e9f79d0360" region_check: true fast_boot: Type2 @@ -767,7 +829,8 @@ files: standalone_path: "bios/ps2_scph50001_0325.bin" description: "PS2, SCPH-50001 (v5.0 03/25/03 A)" region: [north-america, latin-america] - required: false + group: ps-bios + required: true md5: "8aa12ce243210128c5074552d3b86251" region_check: true fast_boot: Type2 @@ -778,7 +841,8 @@ files: standalone_path: "bios/ps2_dtlh50009.bin" description: "PS2, DTL-H50009 (v5.0 02/24/03 J)" region: [japan] - required: false + group: ps-bios + required: true md5: "240d4c5ddd4b54069bdc4a3cd2faf99d" region_check: true fast_boot: Type2 @@ -789,7 +853,8 @@ files: standalone_path: "bios/ps2_desr5000.bin" description: "PS2, DESR-5000/DESR-5100/DESR-7000/DESR-7100 (v5.0 10/28/03 J)" region: [japan] - required: false + group: ps-bios + required: true md5: "1c6cd089e6c83da618fbf2a081eb4888" region_check: true fast_boot: Type2 @@ -800,7 +865,8 @@ files: standalone_path: "bios/ps2_scph55000_0623.bin" description: "PS2, SCPH-55000 (v5.0 06/23/03 J)" region: [japan] - required: false + group: ps-bios + required: true md5: "463d87789c555a4a7604e97d7db545d1" region_check: true fast_boot: Type2 @@ -811,7 +877,8 @@ files: standalone_path: "bios/ps2_dtlh50001_0623.bin" description: "PS2, DTL-H50001 (v5.0 06/23/03 A)" region: [north-america, latin-america] - required: false + group: ps-bios + required: true md5: "ab9d49ad40ae49f19856ad187777b1b3" region_check: true fast_boot: Type2 @@ -822,7 +889,8 @@ files: standalone_path: "bios/ps2_scph50001_0623.bin" description: "PS2, SCPH-50001/SCPH-50010 (v5.0 06/23/03 A)" region: [north-america, latin-america] - required: false + group: ps-bios + required: true md5: "35461cecaa51712b300b2d6798825048" region_check: true fast_boot: Type2 @@ -833,7 +901,8 @@ files: standalone_path: "bios/ps2_scph50002_0623.bin" description: "PS2, SCPH-50002/SCPH-50003/SCPH-50004 (v5.0 06/23/03 E)" region: [europe, oceania] - required: false + group: ps-bios + required: true md5: "bd6415094e1ce9e05daabe85de807666" region_check: true fast_boot: Type2 @@ -844,7 +913,8 @@ files: standalone_path: "bios/ps2_scph50006.bin" description: "PS2, SCPH-50006/SCPH-50007 (v5.0 06/23/03 J)" region: [japan] - required: false + group: ps-bios + required: true md5: "2e70ad008d4ec8549aada8002fdf42fb" region_check: true fast_boot: Type2 @@ -855,7 +925,8 @@ files: standalone_path: "bios/ps2_scph50005.bin" description: "PS2, SCPH-50005 (v5.0 06/23/03 J)" region: [japan] - required: false + group: ps-bios + required: true md5: "50d5b97b57d8c9b6534adcb46c2027d4" region_check: true fast_boot: Type2 @@ -866,7 +937,8 @@ files: standalone_path: "bios/ps2_scph50008.bin" description: "PS2, SCPH-50008 (v5.0 06/23/03 E)" region: [europe, oceania] - required: false + group: ps-bios + required: true md5: "b53d51edc7fc086685e31b811dc32aad" region_check: true fast_boot: Type2 @@ -877,7 +949,8 @@ files: standalone_path: "bios/ps2_scph50009.bin" description: "PS2, SCPH-50009 (v5.0 06/23/03 J)" region: [japan] - required: false + group: ps-bios + required: true md5: "1b6e631b536247756287b916f9396872" region_check: true fast_boot: Type2 @@ -888,7 +961,8 @@ files: standalone_path: "bios/ps2_scph50000_0822.bin" description: "PS2, SCPH-50000 (v5.0 08/22/03 J)" region: [japan] - required: false + group: ps-bios + required: true md5: "00da1b177096cfd2532c8fa22b43e667" region_check: true fast_boot: Type2 @@ -899,7 +973,8 @@ files: standalone_path: "bios/ps2_scph50004_0822.bin" description: "PS2, SCPH-50004 (v5.0 08/22/03 E)" region: [europe, oceania] - required: false + group: ps-bios + required: true md5: "afde410bd026c16be605a1ae4bd651fd" region_check: true fast_boot: Type2 @@ -910,7 +985,8 @@ files: standalone_path: "bios/ps2_scph50011.bin" description: "PS2, SCPH-50011 (v5.0 03/29/04 A)" region: [north-america, latin-america] - required: false + group: ps-bios + required: true md5: "81f4336c1de607dd0865011c0447052e" region_check: true fast_boot: Type2 @@ -921,7 +997,8 @@ files: standalone_path: "bios/ps2_scph70000.bin" description: "PS2, SCPH-70000 (v5.0 06/14/04 J)" region: [japan] - required: false + group: ps-bios + required: true md5: "0eee5d1c779aa50e94edd168b4ebf42e" region_check: true fast_boot: Type2 @@ -932,7 +1009,8 @@ files: standalone_path: "bios/ps2_scph70001.bin" description: "PS2, SCPH-70001/SCPH-70011/SCPH-70012 (v5.0 06/14/04 A)" region: [north-america, latin-america] - required: false + group: ps-bios + required: true md5: "d333558cc14561c1fdc334c75d5f37b7" region_check: true fast_boot: Type2 @@ -943,7 +1021,8 @@ files: standalone_path: "bios/ps2_scph70002.bin" description: "PS2, SCPH-70002/SCPH-70003/SCPH-70004/SCPH-70008 (v5.0 06/14/04 E)" region: [europe, oceania] - required: false + group: ps-bios + required: true md5: "dc752f160044f2ed5fc1f4964db2a095" region_check: true fast_boot: Type2 @@ -954,7 +1033,8 @@ files: standalone_path: "bios/ps2_scph70002_alt.bin" description: "PS2, SCPH-70002 (v5.0 06/14/04 E)" region: [europe, oceania] - required: false + group: ps-bios + required: true md5: "7ebb4fc5eab6f79a27d76ac9aad392b2" region_check: true fast_boot: Type2 @@ -965,7 +1045,8 @@ files: standalone_path: "bios/ps2_dtlh70002.bin" description: "PS2, DTL-H70002 (v5.0 06/14/04 E)" region: [europe, oceania] - required: false + group: ps-bios + required: true md5: "63ead1d74893bf7f36880af81f68a82d" region_check: true fast_boot: Type2 @@ -976,7 +1057,8 @@ files: standalone_path: "bios/ps2_scph70005.bin" description: "PS2, SCPH-70005/SCPH-70006/SCPH-70007 (v5.0 06/14/04 J)" region: [japan] - required: false + group: ps-bios + required: true md5: "3e3e030c0f600442fa05b94f87a1e238" region_check: true fast_boot: Type2 @@ -987,7 +1069,8 @@ files: standalone_path: "bios/ps2_desr5500.bin" description: "PS2, DESR-5500/DESR-5700/DESR-7500/DESR-7700 (v5.0 09/17/04 J)" region: [japan] - required: false + group: ps-bios + required: true md5: "1ad977bb539fc9448a08ab276a836bbc" region_check: true fast_boot: Type2 @@ -998,7 +1081,8 @@ files: standalone_path: "bios/ps2_dtlh75000.bin" description: "PS2, DTL-H75000 (v5.0 06/20/05 J)" region: [japan] - required: false + group: ps-bios + required: true md5: "bf0078ba5e19d57eae18047407f3b6e5" region_check: true fast_boot: Type2 @@ -1009,7 +1093,8 @@ files: standalone_path: "bios/ps2_scph75000.bin" description: "PS2, SCPH-75000 (v5.0 06/20/05 J)" region: [japan] - required: false + group: ps-bios + required: true md5: "eb4f40fcf4911ede39c1bbfe91e7a89a" region_check: true fast_boot: Type2 @@ -1020,7 +1105,8 @@ files: standalone_path: "bios/ps2_dtlh75000a.bin" description: "PS2, DTL-H75000A (v5.0 06/20/05 A)" region: [north-america, latin-america] - required: false + group: ps-bios + required: true md5: "9959ad7a8685cad66206e7752ca23f8b" region_check: true fast_boot: Type2 @@ -1031,7 +1117,8 @@ files: standalone_path: "bios/ps2_scph75001.bin" description: "PS2, SCPH-75001/SCPH-75010 (v5.0 06/20/05 A)" region: [north-america, latin-america] - required: false + group: ps-bios + required: true md5: "929a14baca1776b00869f983aa6e14d2" region_check: true fast_boot: Type2 @@ -1042,7 +1129,8 @@ files: standalone_path: "bios/ps2_scph75002.bin" description: "PS2, SCPH-75002/SCPH-75003/SCPH-75004/SCPH-75008 (v5.0 06/20/05 E)" region: [europe, oceania] - required: false + group: ps-bios + required: true md5: "573f7d4a430c32b3cc0fd0c41e104bbd" region_check: true fast_boot: Type2 @@ -1053,7 +1141,8 @@ files: standalone_path: "bios/ps2_scph75006.bin" description: "PS2, SCPH-75006 (v5.0 06/20/05 J)" region: [japan] - required: false + group: ps-bios + required: true md5: "df63a604e8bff5b0599bd1a6c2721bd0" region_check: true fast_boot: Type2 @@ -1064,7 +1153,8 @@ files: standalone_path: "bios/ps2_scph77000.bin" description: "PS2, SCPH-77000 (v5.0 02/10/06 J)" region: [japan] - required: false + group: ps-bios + required: true md5: "5b1ba4bb914406fae75ab8e38901684d" region_check: true fast_boot: Type2 @@ -1075,7 +1165,8 @@ files: standalone_path: "bios/ps2_scph77001.bin" description: "PS2, SCPH-77001/SCPH-77010 (v5.0 02/10/06 A)" region: [north-america, latin-america] - required: false + group: ps-bios + required: true md5: "cb801b7920a7d536ba07b6534d2433ca" region_check: true fast_boot: Type2 @@ -1086,7 +1177,8 @@ files: standalone_path: "bios/ps2_scph77002.bin" description: "PS2, SCPH-77002/SCPH-77003/SCPH-77004/SCPH-77008 (v5.0 02/10/06 E)" region: [europe, oceania] - required: false + group: ps-bios + required: true md5: "af60e6d1a939019d55e5b330d24b1c25" region_check: true fast_boot: Type2 @@ -1097,7 +1189,8 @@ files: standalone_path: "bios/ps2_scph77006.bin" description: "PS2, SCPH-77006/SCPH-77007 (v5.0 02/10/06 J)" region: [japan] - required: false + group: ps-bios + required: true md5: "549a66d0c698635ca9fa3ab012da7129" region_check: true fast_boot: Type2 @@ -1108,7 +1201,8 @@ files: standalone_path: "bios/ps2_dtlh90000_j.bin" description: "PS2, DTL-H90000 (v5.0 09/05/06 J)" region: [japan] - required: false + group: ps-bios + required: true md5: "5e2014472c88f74f7547d8c2c60eca45" region_check: true fast_boot: Type2 @@ -1119,7 +1213,8 @@ files: standalone_path: "bios/ps2_scph79000_90000.bin" description: "PS2, SCPH-79000/SCPH-90000 (v5.0 09/05/06 J)" region: [japan] - required: false + group: ps-bios + required: true md5: "5de9d0d730ff1e7ad122806335332524" region_check: true fast_boot: Type2 @@ -1130,7 +1225,8 @@ files: standalone_path: "bios/ps2_dtlh90000_a.bin" description: "PS2, DTL-H90000 (v5.0 09/05/06 A)" region: [north-america, latin-america] - required: false + group: ps-bios + required: true md5: "21fe4cad111f7dc0f9af29477057f88d" region_check: true fast_boot: Type2 @@ -1141,7 +1237,8 @@ files: standalone_path: "bios/ps2_scph79001_90001.bin" description: "PS2, SCPH-79001/SCPH-79010/SCPH-90001 (v5.0 09/05/06 A)" region: [north-america, latin-america] - required: false + group: ps-bios + required: true md5: "40c11c063b3b9409aa5e4058e984e30c" region_check: true fast_boot: Type2 @@ -1152,7 +1249,8 @@ files: standalone_path: "bios/ps2_scph79002_90002.bin" description: "PS2, SCPH-79002/SCPH-79003/SCPH-79004/SCPH-79008/SCPH-90002/SCPH-90003/SCPH-90004 (v5.0 09/05/06 E)" region: [europe, oceania] - required: false + group: ps-bios + required: true md5: "80bbb237a6af9c611df43b16b930b683" region_check: true fast_boot: Type2 @@ -1163,7 +1261,8 @@ files: standalone_path: "bios/ps2_scph79006_90006.bin" description: "PS2, SCPH-79006/SCPH-79007/SCPH-90006/SCPH-90007 (v5.0 09/05/06 J)" region: [japan] - required: false + group: ps-bios + required: true md5: "c37bce95d32b2be480f87dd32704e664" region_check: true fast_boot: Type2 @@ -1174,7 +1273,8 @@ files: standalone_path: "bios/ps2_scph90000_0220.bin" description: "PS2, SCPH-90000 (v5.0 02/20/08 J)" region: [japan] - required: false + group: ps-bios + required: true md5: "80ac46fa7e77b8ab4366e86948e54f83" region_check: true fast_boot: Type2 @@ -1185,7 +1285,8 @@ files: standalone_path: "bios/ps2_scph90001_0220.bin" description: "PS2, SCPH-90001/SCPH-90010 (v5.0 02/20/08 A)" region: [north-america, latin-america] - required: false + group: ps-bios + required: true md5: "21038400dc633070a78ad53090c53017" region_check: true fast_boot: Type2 @@ -1196,7 +1297,8 @@ files: standalone_path: "bios/ps2_scph90002_0220.bin" description: "PS2, SCPH-90002/SCPH-90003/SCPH-90004/SCPH-90008 (v5.0 02/20/08 E)" region: [europe, oceania] - required: false + group: ps-bios + required: true md5: "dc69f0643a3030aaa4797501b483d6c4" region_check: true fast_boot: Type2 @@ -1207,7 +1309,8 @@ files: standalone_path: "bios/ps2_scph90005_0220.bin" description: "PS2, SCPH-90005/SCPH-90006/SCPH-90007 (v5.0 02/20/08 J)" region: [japan] - required: false + group: ps-bios + required: true md5: "30d56e79d89fbddf10938fa67fe3f34e" region_check: true fast_boot: Type2 @@ -1218,7 +1321,8 @@ files: standalone_path: "bios/ps2_kdl22px300.bin" description: "PS2, KDL-22PX300 (v5.0 04/15/10 E)" region: [europe, oceania] - required: false + group: ps-bios + required: true md5: "93ea3bcee4252627919175ff1b16a1d9" region_check: true fast_boot: Type2 @@ -1232,7 +1336,8 @@ files: standalone_path: "bios/openbios.bin" description: "OpenBIOS" region: [world] - required: false + group: ps-bios + required: true md5: null region_check: false fast_boot: unsupported diff --git a/emulators/freechaf.yml b/emulators/freechaf.yml index 527674410..5b8681430 100644 --- a/emulators/freechaf.yml +++ b/emulators/freechaf.yml @@ -40,6 +40,7 @@ notes: | files: - name: "sl31253.bin" system: fairchild-channelf + group: psu1 priority: 2 required: false hle_fallback: true @@ -55,6 +56,7 @@ files: - name: "sl90025.bin" system: fairchild-channelf + group: psu1 priority: 1 required: false hle_fallback: true diff --git a/emulators/kronos.yml b/emulators/kronos.yml index 3b8d90937..43b4fb0f4 100644 --- a/emulators/kronos.yml +++ b/emulators/kronos.yml @@ -55,20 +55,32 @@ files: - name: "saturn_bios.bin" path: "kronos/saturn_bios.bin" system: sega-saturn + group: saturn-bios priority: 1 required: true size: 524288 validation: [size] note: "Primary Saturn BIOS. Any region accepted. Searched in kronos/ subdir first." - source_ref: "source yabause/src/libretro/libretro.c:1619-1623" + source_ref: "source yabause/src/libretro/libretro.c:1619-1620" + + - name: "saturn_bios.bin" + system: sega-saturn + group: saturn-bios + priority: 2 + required: true + size: 524288 + validation: [size] + note: "The same name in the system directory, read when kronos/ holds none." + source_ref: "source yabause/src/libretro/libretro.c:1623-1624" # ----------------------------------------------------------- # Saturn BIOS - Japan v1.01 (fallback) # ----------------------------------------------------------- - name: "sega_101.bin" system: sega-saturn - priority: 2 - required: false + group: saturn-bios + priority: 3 + required: true size: 524288 validation: [size] note: "Saturn BIOS v1.01 (Japan). Fallback if saturn_bios.bin missing." @@ -79,8 +91,9 @@ files: # ----------------------------------------------------------- - name: "mpr-17933.bin" system: sega-saturn - priority: 3 - required: false + group: saturn-bios + priority: 4 + required: true size: 524288 validation: [size] note: "Saturn BIOS (NA/EU). Last fallback for Saturn mode." diff --git a/emulators/numero.yml b/emulators/numero.yml index 41727febb..d750e5ab5 100644 --- a/emulators/numero.yml +++ b/emulators/numero.yml @@ -43,23 +43,26 @@ files: - name: "ti83se.rom" system: ti-83 description: "TI-83 Plus Silver Edition ROM dump" + group: rom priority: 1 - required: false + required: true source_ref: "source libnumero/libretro/libretronew.cpp:576-578" note: "Checked first. Recommended for largest storage capacity." - name: "ti83plus.rom" system: ti-83 description: "TI-83 Plus ROM dump" + group: rom priority: 2 - required: false + required: true source_ref: "source libnumero/libretro/libretronew.cpp:583-585" note: "Checked second, used if ti83se.rom is missing." - name: "ti83.rom" system: ti-83 description: "TI-83 ROM dump" + group: rom priority: 3 - required: false + required: true source_ref: "source libnumero/libretro/libretronew.cpp:591-593" note: "Checked last. Original TI-83 (non-Plus)." diff --git a/emulators/odin.yml b/emulators/odin.yml index a0b46e398..bfdf1ac9c 100644 --- a/emulators/odin.yml +++ b/emulators/odin.yml @@ -83,7 +83,8 @@ files: - name: "epr-23400a.ic94" archive: hikaru.zip system: sega-hikaru - required: false + group: boot-rom + required: true variant_group: "hikaru-bootrom" priority: 1 size: 2097152 @@ -95,7 +96,8 @@ files: - name: "epr-23400.ic94" archive: hikaru.zip system: sega-hikaru - required: false + group: boot-rom + required: true variant_group: "hikaru-bootrom" priority: 2 size: 2097152 @@ -107,7 +109,8 @@ files: - name: "epr-21904.ic94" archive: hikaru.zip system: sega-hikaru - required: false + group: boot-rom + required: true variant_group: "hikaru-bootrom" priority: 3 size: 2097152 diff --git a/emulators/pcsx1.yml b/emulators/pcsx1.yml index 6191bc337..bc4c8fbf2 100644 --- a/emulators/pcsx1.yml +++ b/emulators/pcsx1.yml @@ -25,6 +25,7 @@ notes: > files: - name: "scph1001.bin" description: "SCPH-1001 (v2.2 12-04-95 A)" + group: bios priority: 1 required: false hle_fallback: true @@ -37,6 +38,7 @@ files: - name: "scph5501.bin" description: "SCPH-5501 (v3.0 11-18-96 A)" + group: bios priority: 2 required: false hle_fallback: true @@ -49,6 +51,7 @@ files: - name: "scph7001.bin" description: "SCPH-7001 (v4.1 12-16-97 A)" + group: bios priority: 3 required: false hle_fallback: true diff --git a/emulators/picodrive.yml b/emulators/picodrive.yml index 027f87c02..17d67015d 100644 --- a/emulators/picodrive.yml +++ b/emulators/picodrive.yml @@ -71,6 +71,7 @@ files: system: sega-megacd variant_group: "mega-cd-bios" region: [north-america, latin-america] + group: mega-cd-us priority: 1 required: true size: 131072 # 128 KB (0x20000) @@ -81,8 +82,9 @@ files: system: sega-megacd variant_group: "mega-cd-bios" region: [north-america, latin-america] + group: mega-cd-us priority: 2 - required: false + required: true size: 131072 note: "US Sega CD BIOS (March 1993). Second in US search order." source_ref: "source platform/libretro/libretro.c:1273" @@ -91,8 +93,9 @@ files: system: sega-megacd variant_group: "mega-cd-bios" region: [north-america, latin-america] + group: mega-cd-us priority: 3 - required: false + required: true size: 131072 note: "US Sega CD Model 1 BIOS (October 1992). Third in US search order." source_ref: "source platform/libretro/libretro.c:1273" @@ -101,8 +104,9 @@ files: system: sega-megacd variant_group: "mega-cd-bios" region: [north-america, latin-america] + group: mega-cd-us priority: 4 - required: false + required: true size: 131072 note: "US Sega CD BIOS (generic name). Last in US search order." source_ref: "source platform/libretro/libretro.c:1273" @@ -114,6 +118,7 @@ files: system: sega-megacd variant_group: "mega-cd-bios" region: [europe, oceania] + group: mega-cd-eu priority: 1 required: true size: 131072 @@ -124,8 +129,9 @@ files: system: sega-megacd variant_group: "mega-cd-bios" region: [europe, oceania] + group: mega-cd-eu priority: 2 - required: false + required: true size: 131072 note: "EU Mega CD Model 2 BIOS (March 1993). Second in EU search order." source_ref: "source platform/libretro/libretro.c:1276" @@ -134,8 +140,9 @@ files: system: sega-megacd variant_group: "mega-cd-bios" region: [europe, oceania] + group: mega-cd-eu priority: 3 - required: false + required: true size: 131072 note: "EU Mega CD Model 1 BIOS (October 1992). Third in EU search order." source_ref: "source platform/libretro/libretro.c:1276" @@ -144,8 +151,9 @@ files: system: sega-megacd variant_group: "mega-cd-bios" region: [europe, oceania] + group: mega-cd-eu priority: 4 - required: false + required: true size: 131072 note: "EU Mega CD BIOS (generic name). Last in EU search order." source_ref: "source platform/libretro/libretro.c:1276" @@ -157,6 +165,7 @@ files: system: sega-megacd variant_group: "mega-cd-bios" region: [japan] + group: mega-cd-jp priority: 1 required: true size: 131072 @@ -167,8 +176,9 @@ files: system: sega-megacd variant_group: "mega-cd-bios" region: [japan] + group: mega-cd-jp priority: 2 - required: false + required: true size: 131072 note: "JP Mega CD Model 1 BIOS (December 1991). Second in JP search order." source_ref: "source platform/libretro/libretro.c:1279" @@ -177,8 +187,9 @@ files: system: sega-megacd variant_group: "mega-cd-bios" region: [japan] + group: mega-cd-jp priority: 3 - required: false + required: true size: 131072 note: "JP Mega CD Model 1 BIOS (November 1991). Third in JP search order." source_ref: "source platform/libretro/libretro.c:1279" @@ -187,8 +198,9 @@ files: system: sega-megacd variant_group: "mega-cd-bios" region: [japan] + group: mega-cd-jp priority: 4 - required: false + required: true size: 131072 note: "JP Mega CD BIOS (generic name). Last in JP search order." source_ref: "source platform/libretro/libretro.c:1279" diff --git a/emulators/px68k-sdl.yml b/emulators/px68k-sdl.yml index c618d3473..eeee0254f 100644 --- a/emulators/px68k-sdl.yml +++ b/emulators/px68k-sdl.yml @@ -51,6 +51,7 @@ files: - name: "iplrom.dat" system: sharp-x68000 path: "iplrom.dat" + group: ipl priority: 1 required: true size: 131072 @@ -64,8 +65,9 @@ files: - name: "iplromxv.dat" system: sharp-x68000 path: "iplromxv.dat" + group: ipl priority: 2 - required: false + required: true size: 131072 source_ref: "SDL/SDL2/winx68k.cpp:277-279,288-307" note: "X68000 XVI IPL ROM, read when iplrom.dat is absent." @@ -73,8 +75,9 @@ files: - name: "iplromco.dat" system: sharp-x68000 path: "iplromco.dat" + group: ipl priority: 3 - required: false + required: true size: 131072 source_ref: "SDL/SDL2/winx68k.cpp:277-279,288-307" note: "X68000 Compact IPL ROM, read when the first two names are absent." @@ -82,8 +85,9 @@ files: - name: "iplrom30.dat" system: sharp-x68000 path: "iplrom30.dat" + group: ipl priority: 4 - required: false + required: true size: 131072 source_ref: "SDL/SDL2/winx68k.cpp:277-279,288-307" note: "X68030 IPL ROM, read when the three other names are absent." @@ -91,8 +95,9 @@ files: - name: "cgrom30.dat" system: sharp-x68000 path: "cgrom30.dat" + group: font priority: 1 - required: false + required: true size: 786432 source_ref: "SDL/SDL2/winx68k.cpp:280-282,317-331" note: > @@ -103,8 +108,9 @@ files: - name: "cgrom30.tmp" system: sharp-x68000 path: "cgrom30.tmp" + group: font priority: 2 - required: false + required: true unsourceable: "rendered from TrueType fonts by the font generator, no canonical file" size: 786432 source_ref: "SDL/SDL2/winx68k.cpp:280-282,317-331" @@ -113,6 +119,7 @@ files: - name: "cgrom.dat" system: sharp-x68000 path: "cgrom.dat" + group: font priority: 3 required: true size: 786432 @@ -122,8 +129,9 @@ files: - name: "cgrom.tmp" system: sharp-x68000 path: "cgrom.tmp" + group: font priority: 4 - required: false + required: true unsourceable: "rendered from TrueType fonts by the font generator, no canonical file" size: 786432 source_ref: "SDL/SDL2/winx68k.cpp:280-282,317-331, SDL/tool/create_cgrom.c:20" diff --git a/emulators/px68k.yml b/emulators/px68k.yml index c3b4ec4b3..1f0bbe529 100644 --- a/emulators/px68k.yml +++ b/emulators/px68k.yml @@ -48,6 +48,7 @@ files: - name: "iplrom.dat" installed_on: [recalbox] path: "keropi/iplrom.dat" + group: ipl priority: 1 size: 131072 required: true @@ -59,9 +60,10 @@ files: - name: "iplrom30.dat" path: "keropi/iplrom30.dat" + group: ipl priority: 2 size: 131072 - required: false + required: true note: > X68030 IPL ROM. Alternative to iplrom.dat for X68030 model emulation. Only used if iplrom.dat is not found. @@ -69,9 +71,10 @@ files: - name: "iplromco.dat" path: "keropi/iplromco.dat" + group: ipl priority: 3 size: 131072 - required: false + required: true note: > X68000 Compact IPL ROM. Alternative to iplrom.dat. Only used if iplrom.dat and iplrom30.dat are not found. @@ -79,9 +82,10 @@ files: - name: "iplromxv.dat" path: "keropi/iplromxv.dat" + group: ipl priority: 4 size: 131072 - required: false + required: true note: > X68000 XVI IPL ROM. Last fallback in the IPL search order. Only used if the three other IPL variants are not found. @@ -93,6 +97,8 @@ files: installed_on: [recalbox] path: "keropi/cgrom.dat" size: 786432 + group: font + priority: 1 required: true note: > X68000 character generator ROM. 768 KB, provides the font glyphs for text @@ -103,7 +109,9 @@ files: - name: "cgrom.tmp" path: "keropi/cgrom.tmp" size: 786432 - required: false + group: font + priority: 2 + required: true unsourceable: "generated by WinX68k from the host fonts, no canonical file" note: > Alternative font file, used as fallback when cgrom.dat is absent. diff --git a/emulators/yabadabadoo.yml b/emulators/yabadabadoo.yml index 1f27a4d77..32f1625a8 100644 --- a/emulators/yabadabadoo.yml +++ b/emulators/yabadabadoo.yml @@ -61,6 +61,7 @@ notes: | files: - name: "saturn_bios.bin" system: sega-saturn + group: saturn-bios required: true max_size: 524288 validation: [max_size] @@ -69,7 +70,8 @@ files: - name: "sega_101.bin" system: sega-saturn - required: false + group: saturn-bios + required: true region: [japan, asia-ntsc, south-korea] max_size: 524288 validation: [max_size] @@ -78,7 +80,8 @@ files: - name: "mpr-17933.bin" system: sega-saturn - required: false + group: saturn-bios + required: true region: [north-america, europe, oceania, asia-pal, brazil, latin-america] max_size: 524288 validation: [max_size] diff --git a/emulators/yabasanshiro.yml b/emulators/yabasanshiro.yml index b5ed0c276..7c281eb80 100644 --- a/emulators/yabasanshiro.yml +++ b/emulators/yabasanshiro.yml @@ -47,9 +47,9 @@ notes: | 1. {system}/saturn_bios.bin 2. {system}/sega_101.bin 3. {system}/mpr-17933.bin - First file found wins. Real BIOS required for normal operation. - HLE available via yabasanshiro_force_hle_bios core option (deprecated, - debug only). + First file found wins. Without any of the three the core refuses the + game, even with the yabasanshiro_force_hle_bios core option set + (yabause/src/libretro/libretro.c:1064-1071). Addon cartridges: 1M/4M RAM expansion via core option (no files needed). ROM cartridge and MPEG card ROM loading disabled in libretro port @@ -84,26 +84,26 @@ files: - name: "saturn_bios.bin" system: sega-saturn mode: libretro + group: saturn-bios priority: 1 required: true - hle_fallback: true note: "Primary Saturn BIOS. Any region accepted." source_ref: "yabause/src/libretro/libretro.c:1048" - name: "sega_101.bin" system: sega-saturn mode: libretro + group: saturn-bios priority: 2 - required: false - hle_fallback: true + required: true note: "Saturn BIOS v1.01 (Japan). Fallback if saturn_bios.bin missing." source_ref: "yabause/src/libretro/libretro.c:1052" - name: "mpr-17933.bin" system: sega-saturn mode: libretro + group: saturn-bios priority: 3 - required: false - hle_fallback: true + required: true note: "Saturn BIOS v1.003 (NA/EU). Last fallback." source_ref: "yabause/src/libretro/libretro.c:1056" diff --git a/emulators/yabause.yml b/emulators/yabause.yml index 2140005e7..e086cab20 100644 --- a/emulators/yabause.yml +++ b/emulators/yabause.yml @@ -50,6 +50,7 @@ notes: | files: - name: "saturn_bios.bin" system: sega-saturn + group: saturn-bios priority: 1 required: true hle_fallback: true @@ -60,8 +61,9 @@ files: - name: "sega_101.bin" system: sega-saturn + group: saturn-bios priority: 2 - required: false + required: true hle_fallback: true max_size: 524288 validation: [max_size] @@ -70,8 +72,9 @@ files: - name: "mpr-17933.bin" system: sega-saturn + group: saturn-bios priority: 3 - required: false + required: true hle_fallback: true max_size: 524288 validation: [max_size] diff --git a/emulators/ymir.yml b/emulators/ymir.yml index 27d85df6d..507235e2e 100644 --- a/emulators/ymir.yml +++ b/emulators/ymir.yml @@ -96,6 +96,7 @@ files: - name: "sega_101.bin" system: sega-saturn region: [japan] + group: ipl priority: 1 required: true mode: libretro @@ -110,6 +111,7 @@ files: - name: "mpr-17933.bin" system: sega-saturn region: [north-america, europe, asia-ntsc, asia-pal, brazil, latin-america] + group: ipl priority: 2 required: true mode: libretro @@ -123,8 +125,9 @@ files: - name: "saturn_bios.bin" system: sega-saturn + group: ipl priority: 3 - required: false + required: true mode: libretro size: 524288 validation: [size] From 315dab82b72828ddd18974f66ca83d3c25c6d507 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Mon, 9 Nov 2026 18:19:32 +0100 Subject: [PATCH 3/6] feat: give each set its requirement and one pick Three changes to the search-list model, one commit because the readers and the profiles must move together for the schema to hold. Set requirement in a groups map. pom2 falls back on apple2.rom for the ][ and the ][+, and on apple2e.rom for the //e it boots and for the unenhanced //e: one required on the file could not say both. A profile now declares groups: {name: {required}}, a member writes no required of its own (schema and validate_schemas refuse one) and load_emulator_ profiles derives it, true when one of its sets is. group takes a name, a list, or a map from set to rank where the rank differs per set. pom2 is grouped from SystemProfile.cpp and MainWindow.cpp:246-296 at its pin. One member per required set under --required-only. The pick is the first the code reads among the held members the region filter leaves, by rank then declaration order; a set the platform declares a member of is carried by that declaration. The emulator pack, verify --emulator, the platform pack, its manifest and verify --platform read alternatives.required_only_spares or core_file_spares. variant_group is now selection_slot: files a core chooses between by a rule other than first found (disc territory for PicoDrive and Pizza Boy SC, the o2em_bios option, a WebAssembly probe in SWF Player). Where the code takes any one, the field became a group: ARMSX1 (ArmSxBackend.java:149-157, name order), ePSXe (load_bios 0x433540-0x433621 of the 2.0.5 Linux x64 binary, one configured path, HLE when BiosHLE is set), FPse and FPse-NG, DXX-Rebirth's HOGs (inferno.cpp:472-512). odin and zesarux carried it with no effect. slot.py compares ranks only between candidates that share a set. --- emulators/armsx1.yml | 104 +++++++++++----------- emulators/duckstation.yml | 109 +---------------------- emulators/dxx-rebirth.yml | 20 +++-- emulators/epsxe.yml | 73 ++++++---------- emulators/fpse-ng.yml | 7 +- emulators/fpse.yml | 7 +- emulators/freechaf.yml | 6 +- emulators/kronos.yml | 8 +- emulators/numero.yml | 7 +- emulators/o2em.yml | 8 +- emulators/odin.yml | 10 +-- emulators/pcsx1.yml | 7 +- emulators/picodrive.yml | 44 +++++----- emulators/pizza-boy-sc.yml | 12 +-- emulators/pom2.yml | 68 ++++++++------- emulators/px68k-sdl.yml | 14 ++- emulators/px68k.yml | 12 +-- emulators/swf-player.yml | 8 +- emulators/yabadabadoo.yml | 7 +- emulators/yabasanshiro.yml | 7 +- emulators/yabause.yml | 7 +- emulators/ymir.yml | 7 +- emulators/zesarux.yml | 1 - schemas/emulator.schema.json | 24 ++++- scripts/alternatives.py | 164 ++++++++++++++++++++++++++++++----- scripts/common.py | 3 + scripts/cross_reference.py | 15 ++-- scripts/generate_pack.py | 30 +++++-- scripts/generate_site.py | 8 +- scripts/launchers.py | 2 +- scripts/packextras.py | 59 ++++++++++--- scripts/slot.py | 43 +++++++-- scripts/validate_schemas.py | 32 ++++--- scripts/verify.py | 53 ++++++++--- tests/test_e2e.py | 90 +++++++++++++++---- tests/test_region_groups.py | 8 +- wiki/architecture.md | 2 +- wiki/profiling.md | 7 +- 38 files changed, 661 insertions(+), 432 deletions(-) diff --git a/emulators/armsx1.yml b/emulators/armsx1.yml index 24a435184..9cc8570a5 100644 --- a/emulators/armsx1.yml +++ b/emulators/armsx1.yml @@ -71,22 +71,26 @@ notes: | frontend/config.c:1384-1405, frontend/config.c:1541-1560, frontend/main.cpp:2555-2561, psx/dev/bios.c:22-75 +groups: + ps-bios: + required: true + files: - name: "scph1000.bin" + group: ps-bios + priority: 4 description: "SCPH-1000, DTL-H1000 (v1.0)" region: [japan] - required: false - variant_group: "psx-bios" size: 524288 md5: "239665b1a3dade1b5a52c06338011044" validation: [size, md5] source_ref: "android/app/src/main/java/com/armsx2/core/Ps1BiosProbe.java:30" - name: "scph1001.bin" + group: ps-bios + priority: 6 description: "SCPH-1001, 5003, DTL-H1201, H3001 (v2.2 12-04-95 A)" region: [north-america, latin-america] - required: false - variant_group: "psx-bios" size: 524288 md5: "924e392ed05558ffdb115408c263dccf" validation: [size, md5] @@ -96,230 +100,230 @@ files: source_ref: "android/app/src/main/java/com/armsx2/core/Ps1BiosProbe.java:31" - name: "scph1002a.bin" + group: ps-bios + priority: 9 description: "SCPH-1002, DTL-H1002 (v2.0 05-10-95 E)" region: [europe, oceania] - required: false - variant_group: "psx-bios" size: 524288 md5: "54847e693405ffeb0359c6287434cbef" validation: [size, md5] source_ref: "android/app/src/main/java/com/armsx2/core/Ps1BiosProbe.java:32" - name: "scph1002b.bin" + group: ps-bios + priority: 10 description: "SCPH-1002, DTL-H1102 (v2.1 07-17-95 E)" region: [europe, oceania] - required: false - variant_group: "psx-bios" size: 524288 md5: "417b34706319da7cf001e76e40136c23" validation: [size, md5] source_ref: "android/app/src/main/java/com/armsx2/core/Ps1BiosProbe.java:33" - name: "scph1002c.bin" + group: ps-bios + priority: 11 description: "SCPH-1002, DTL-H1202, H3002 (v2.2 12-04-95 E)" region: [europe, oceania] - required: false - variant_group: "psx-bios" size: 524288 md5: "e2110b8a2b97a8e0b857a45d32f7e187" validation: [size, md5] source_ref: "android/app/src/main/java/com/armsx2/core/Ps1BiosProbe.java:34" - name: "dtlh1100.bin" + group: ps-bios + priority: 1 description: "DTL-H1100 (v2.2 03-06-96 D)" region: [japan] - required: false - variant_group: "psx-bios" size: 524288 md5: "ca5cfc321f916756e3f0effbfaeba13b" validation: [size, md5] source_ref: "android/app/src/main/java/com/armsx2/core/Ps1BiosProbe.java:35" - name: "scph3000.bin" + group: ps-bios + priority: 16 description: "SCPH-3000, DTL-H1000H (v1.1 01-22-95)" region: [japan] - required: false - variant_group: "psx-bios" size: 524288 md5: "849515939161e62f6b866f6853006780" validation: [size, md5] source_ref: "android/app/src/main/java/com/armsx2/core/Ps1BiosProbe.java:36" - name: "scph1001_v20.bin" + group: ps-bios + priority: 7 description: "SCPH-1001, DTL-H1001 (v2.0 05-07-95 A)" region: [north-america, latin-america] - required: false - variant_group: "psx-bios" size: 524288 md5: "dc2b9bf8da62ec93e868cfd29f0d067d" validation: [size, md5] source_ref: "android/app/src/main/java/com/armsx2/core/Ps1BiosProbe.java:37" - name: "scph3500.bin" + group: ps-bios + priority: 17 description: "SCPH-3500 (v2.1 07-17-95 J)" region: [japan] - required: false - variant_group: "psx-bios" size: 524288 md5: "cba733ceeff5aef5c32254f1d617fa62" validation: [size, md5] source_ref: "android/app/src/main/java/com/armsx2/core/Ps1BiosProbe.java:38" - name: "scph1001_v21.bin" + group: ps-bios + priority: 8 description: "SCPH-1001, DTL-H1101 (v2.1 07-17-95 A)" region: [north-america, latin-america] - required: false - variant_group: "psx-bios" size: 524288 md5: "da27e8b6dab242d8f91a9b25d80c63b8" validation: [size, md5] source_ref: "android/app/src/main/java/com/armsx2/core/Ps1BiosProbe.java:39" - name: "scph5000.bin" + group: ps-bios + priority: 18 description: "SCPH-5000, DTL-H1200, H3000 (v2.2 12-04-95 J)" region: [japan] - required: false - variant_group: "psx-bios" size: 524288 md5: "57a06303dfa9cf9351222dfcbb4a29d9" validation: [size, md5] source_ref: "android/app/src/main/java/com/armsx2/core/Ps1BiosProbe.java:40" - name: "scph5500.bin" + group: ps-bios + priority: 19 description: "SCPH-5500 (v3.0 09-09-96 J)" region: [japan] - required: false - variant_group: "psx-bios" size: 524288 md5: "8dd7d5296a650fac7319bce665a6a53c" validation: [size, md5] source_ref: "android/app/src/main/java/com/armsx2/core/Ps1BiosProbe.java:41" - name: "scph5501.bin" + group: ps-bios + priority: 20 description: "SCPH-5501, 5503, 7003 (v3.0 11-18-96 A)" region: [north-america, latin-america] - required: false - variant_group: "psx-bios" size: 524288 md5: "490f666e1afb15b7362b406ed1cea246" validation: [size, md5] source_ref: "android/app/src/main/java/com/armsx2/core/Ps1BiosProbe.java:42" - name: "scph5502.bin" + group: ps-bios + priority: 21 description: "SCPH-5502, 5552 (v3.0 01-06-97 E)" region: [europe, oceania] - required: false - variant_group: "psx-bios" size: 524288 md5: "32736f17079d0b2b7024407c39bd3050" validation: [size, md5] source_ref: "android/app/src/main/java/com/armsx2/core/Ps1BiosProbe.java:43" - name: "scph7000.bin" + group: ps-bios + priority: 22 description: "SCPH-7000, 7500, 9000 (v4.0 08-18-97 J)" region: [japan] - required: false - variant_group: "psx-bios" size: 524288 md5: "8e4c14f567745eff2f0408c8129f72a6" validation: [size, md5] source_ref: "android/app/src/main/java/com/armsx2/core/Ps1BiosProbe.java:44" - name: "scph7000w.bin" + group: ps-bios + priority: 23 description: "SCPH-7000W (v4.1 11-14-97 A)" region: [japan] - required: false - variant_group: "psx-bios" size: 524288 md5: "b84be139db3ee6cbd075630aa20a6553" validation: [size, md5] source_ref: "android/app/src/main/java/com/armsx2/core/Ps1BiosProbe.java:45" - name: "scph7001.bin" + group: ps-bios + priority: 24 description: "SCPH-7001, 7501, 7503, 9001, 9003, 9903 (v4.1 12-16-97 A)" region: [north-america, latin-america] - required: false - variant_group: "psx-bios" size: 524288 md5: "1e68c231d0896b7eadcad1d7d8e76129" validation: [size, md5] source_ref: "android/app/src/main/java/com/armsx2/core/Ps1BiosProbe.java:46" - name: "scph7002.bin" + group: ps-bios + priority: 25 description: "SCPH-7002, 7502, 9002 (v4.1 12-16-97 E)" region: [europe, oceania] - required: false - variant_group: "psx-bios" size: 524288 md5: "b9d9a0286c33dc6b7237bb13cd46fdee" validation: [size, md5] source_ref: "android/app/src/main/java/com/armsx2/core/Ps1BiosProbe.java:47" - name: "scph100.bin" + group: ps-bios + priority: 3 description: "SCPH-100 (v4.3 03-11-00 J)" region: [japan] - required: false - variant_group: "psx-bios" size: 524288 md5: "8abc1b549a4a80954addc48ef02c4521" validation: [size, md5] source_ref: "android/app/src/main/java/com/armsx2/core/Ps1BiosProbe.java:48" - name: "scph101_v44.bin" + group: ps-bios + priority: 12 description: "SCPH-101 (v4.4 03-24-00 A)" region: [north-america, latin-america] - required: false - variant_group: "psx-bios" size: 524288 md5: "9a09ab7e49b422c007e6d54d7c49b965" validation: [size, md5] source_ref: "android/app/src/main/java/com/armsx2/core/Ps1BiosProbe.java:49" - name: "scph101_v45.bin" + group: ps-bios + priority: 13 description: "SCPH-101 (v4.5 05-25-00 A)" region: [north-america, latin-america] - required: false - variant_group: "psx-bios" size: 524288 md5: "6e3735ff4c7dc899ee98981385f6f3d0" validation: [size, md5] source_ref: "android/app/src/main/java/com/armsx2/core/Ps1BiosProbe.java:50" - name: "scph102_v44.bin" + group: ps-bios + priority: 14 description: "SCPH-102 (v4.4 03-24-00 E)" region: [europe, oceania] - required: false - variant_group: "psx-bios" size: 524288 md5: "b10f5e0e3d9eb60e5159690680b1e774" validation: [size, md5] source_ref: "android/app/src/main/java/com/armsx2/core/Ps1BiosProbe.java:51" - name: "scph102_v45.bin" + group: ps-bios + priority: 15 description: "SCPH-102 (v4.5 05-25-00 E)" region: [europe, oceania] - required: false - variant_group: "psx-bios" size: 524288 md5: "de93caec13d1a141a40a79f5c86168d6" validation: [size, md5] source_ref: "android/app/src/main/java/com/armsx2/core/Ps1BiosProbe.java:52" - name: "psxonpsp660.bin" + group: ps-bios + priority: 2 description: "PSP, SCPH-1000R (v4.5 05-25-00 J)" region: [world] - required: false - variant_group: "psx-bios" size: 524288 md5: "c53ca5908936d412331790f4426c6c33" validation: [size, md5] source_ref: "android/app/src/main/java/com/armsx2/core/Ps1BiosProbe.java:53" - name: "scph1000r.bin" + group: ps-bios + priority: 5 description: "SCPH-1000R (v4.5 05-25-00 J)" region: [japan] - required: false - variant_group: "psx-bios" size: 524288 md5: "476d68a94ccec3b9c8303bbd1daf2810" validation: [size, md5] diff --git a/emulators/duckstation.yml b/emulators/duckstation.yml index b4c67cd73..739810077 100644 --- a/emulators/duckstation.yml +++ b/emulators/duckstation.yml @@ -74,6 +74,10 @@ notes: | The libretro core was removed by the author (license change to non-commercial). The .info file and buildbot builds still exist from community forks. +groups: + ps-bios: + required: true + files: # -- PS1 BIOS images (priority <= 20, preferred) -- @@ -82,7 +86,6 @@ files: description: "SCPH-1000, DTL-H1000 (v1.0)" region: [japan] group: ps-bios - required: true md5: "239665b1a3dade1b5a52c06338011044" region_check: true fast_boot: Type1 @@ -94,7 +97,6 @@ files: description: "SCPH-1001, 5003, DTL-H1201, H3001 (v2.2 12-04-95 A)" region: [north-america, latin-america] group: ps-bios - required: true md5: "924e392ed05558ffdb115408c263dccf" region_check: false fast_boot: Type1 @@ -106,7 +108,6 @@ files: description: "SCPH-1002, DTL-H1002 (v2.0 05-10-95 E)" region: [europe, oceania] group: ps-bios - required: true md5: "54847e693405ffeb0359c6287434cbef" region_check: false fast_boot: Type1 @@ -118,7 +119,6 @@ files: description: "SCPH-1002, DTL-H1102 (v2.1 07-17-95 E)" region: [europe, oceania] group: ps-bios - required: true md5: "417b34706319da7cf001e76e40136c23" region_check: false fast_boot: Type1 @@ -130,7 +130,6 @@ files: description: "SCPH-1002, DTL-H1202, H3002 (v2.2 12-04-95 E)" region: [europe, oceania] group: ps-bios - required: true md5: "e2110b8a2b97a8e0b857a45d32f7e187" region_check: false fast_boot: Type1 @@ -142,7 +141,6 @@ files: description: "DTL-H1100 (v2.2 03-06-96 D)" region: [japan] group: ps-bios - required: true md5: "ca5cfc321f916756e3f0effbfaeba13b" region_check: true fast_boot: Type1 @@ -154,7 +152,6 @@ files: description: "SCPH-3000, DTL-H1000H (v1.1 01-22-95)" region: [japan] group: ps-bios - required: true md5: "849515939161e62f6b866f6853006780" region_check: true fast_boot: Type1 @@ -166,7 +163,6 @@ files: description: "SCPH-1001, DTL-H1001 (v2.0 05-07-95 A)" region: [north-america, latin-america] group: ps-bios - required: true md5: "dc2b9bf8da62ec93e868cfd29f0d067d" region_check: false fast_boot: Type1 @@ -178,7 +174,6 @@ files: description: "SCPH-3500 (v2.1 07-17-95 J)" region: [japan] group: ps-bios - required: true md5: "cba733ceeff5aef5c32254f1d617fa62" region_check: true fast_boot: Type1 @@ -190,7 +185,6 @@ files: description: "SCPH-1001, DTL-H1101 (v2.1 07-17-95 A)" region: [north-america, latin-america] group: ps-bios - required: true md5: "da27e8b6dab242d8f91a9b25d80c63b8" region_check: false fast_boot: Type1 @@ -202,7 +196,6 @@ files: description: "SCPH-5000, DTL-H1200, H3000 (v2.2 12-04-95 J)" region: [japan] group: ps-bios - required: true md5: "57a06303dfa9cf9351222dfcbb4a29d9" region_check: true fast_boot: Type1 @@ -214,7 +207,6 @@ files: description: "SCPH-5500 (v3.0 09-09-96 J)" region: [japan] group: ps-bios - required: true md5: "8dd7d5296a650fac7319bce665a6a53c" region_check: true fast_boot: Type1 @@ -226,7 +218,6 @@ files: description: "SCPH-5501, 5503, 7003 (v3.0 11-18-96 A)" region: [north-america, latin-america] group: ps-bios - required: true md5: "490f666e1afb15b7362b406ed1cea246" region_check: false fast_boot: Type1 @@ -238,7 +229,6 @@ files: description: "SCPH-5502, 5552 (v3.0 01-06-97 E)" region: [europe, oceania] group: ps-bios - required: true md5: "32736f17079d0b2b7024407c39bd3050" region_check: false fast_boot: Type1 @@ -250,7 +240,6 @@ files: description: "SCPH-7000, 7500, 9000 (v4.0 08-18-97 J)" region: [japan] group: ps-bios - required: true md5: "8e4c14f567745eff2f0408c8129f72a6" region_check: true fast_boot: Type1 @@ -262,7 +251,6 @@ files: description: "SCPH-7000W (v4.1 11-14-97 A)" region: [japan] group: ps-bios - required: true md5: "b84be139db3ee6cbd075630aa20a6553" region_check: false fast_boot: Type1 @@ -274,7 +262,6 @@ files: description: "SCPH-7001, 7501, 7503, 9001, 9003, 9903 (v4.1 12-16-97 A)" region: [north-america, latin-america] group: ps-bios - required: true md5: "1e68c231d0896b7eadcad1d7d8e76129" region_check: false fast_boot: Type1 @@ -286,7 +273,6 @@ files: description: "SCPH-7002, 7502, 9002 (v4.1 12-16-97 E)" region: [europe, oceania] group: ps-bios - required: true md5: "b9d9a0286c33dc6b7237bb13cd46fdee" region_check: false fast_boot: Type1 @@ -298,7 +284,6 @@ files: description: "SCPH-100 (v4.3 03-11-00 J)" region: [japan] group: ps-bios - required: true md5: "8abc1b549a4a80954addc48ef02c4521" region_check: true fast_boot: Type1 @@ -310,7 +295,6 @@ files: description: "SCPH-101 (v4.4 03-24-00 A)" region: [north-america, latin-america] group: ps-bios - required: true md5: "9a09ab7e49b422c007e6d54d7c49b965" region_check: false fast_boot: Type1 @@ -322,7 +306,6 @@ files: description: "SCPH-101 (v4.5 05-25-00 A)" region: [north-america, latin-america] group: ps-bios - required: true md5: "6e3735ff4c7dc899ee98981385f6f3d0" region_check: false fast_boot: Type1 @@ -334,7 +317,6 @@ files: description: "SCPH-102 (v4.4 03-24-00 E)" region: [europe, oceania] group: ps-bios - required: true md5: "b10f5e0e3d9eb60e5159690680b1e774" region_check: true fast_boot: Type1 @@ -346,7 +328,6 @@ files: description: "SCPH-102 (v4.5 05-25-00 E)" region: [europe, oceania] group: ps-bios - required: true md5: "de93caec13d1a141a40a79f5c86168d6" region_check: true fast_boot: Type1 @@ -358,7 +339,6 @@ files: description: "SCPH-1000R (v4.5 05-25-00 J)" region: [japan] group: ps-bios - required: true md5: "476d68a94ccec3b9c8303bbd1daf2810" region_check: true fast_boot: Type1 @@ -374,7 +354,6 @@ files: description: "PS2, SCPH-18000 (v5.0 10-27-00 J)" region: [japan] group: ps-bios - required: true md5: "d8f485717a5237285e4d7c5f881b7f32" region_check: true fast_boot: Type2 @@ -386,7 +365,6 @@ files: description: "PS2, SCPH-30003 (v5.0 09-02-00 E)" region: [europe, oceania] group: ps-bios - required: true md5: "71f50ef4f4e17c163c78908e16244f7d" region_check: true fast_boot: Type2 @@ -398,7 +376,6 @@ files: description: "PS2, DTL-H10000 (v5.0 01/17/00 T)" region: [world] group: ps-bios - required: true md5: "32f2e4d5ff5ee11072a6bc45530f5765" region_check: true fast_boot: Type2 @@ -410,7 +387,6 @@ files: description: "PS2, SCPH-10000 (v5.0 01/17/00 T)" region: [world] group: ps-bios - required: true md5: "acf4730ceb38ac9d8c7d8e21f2614600" region_check: true fast_boot: Type2 @@ -422,7 +398,6 @@ files: description: "PS2, DTL-H10000 (v5.0 02/17/00 T)" region: [world] group: ps-bios - required: true md5: "acf9968c8f596d2b15f42272082513d1" region_check: true fast_boot: Type2 @@ -434,7 +409,6 @@ files: description: "PS2, SCPH-10000/SCPH-15000 (v5.0 02/17/00 T)" region: [world] group: ps-bios - required: true md5: "b1459d7446c69e3e97e6ace3ae23dd1c" region_check: true fast_boot: Type2 @@ -446,7 +420,6 @@ files: description: "PS2, DTL-H10000 (v5.0 02/24/00 T)" region: [world] group: ps-bios - required: true md5: "d3f1853a16c2ec18f3cd1ae655213308" region_check: true fast_boot: Type2 @@ -458,7 +431,6 @@ files: description: "PS2, DTL-H30001 (v5.0 07/27/00 A)" region: [north-america, latin-america] group: ps-bios - required: true md5: "63e6fd9b3c72e0d7b920e80cf76645cd" region_check: true fast_boot: Type2 @@ -470,7 +442,6 @@ files: description: "PS2, SCPH-30001 (v5.0 07/27/00 A)" region: [north-america, latin-america] group: ps-bios - required: true md5: "a20c97c02210f16678ca3010127caf36" region_check: true fast_boot: Type2 @@ -482,7 +453,6 @@ files: description: "PS2, SCPH-30001 (v5.0 09/02/00 A)" region: [north-america, latin-america] group: ps-bios - required: true md5: "8db2fbbac7413bf3e7154c1e0715e565" region_check: true fast_boot: Type2 @@ -494,7 +464,6 @@ files: description: "PS2, DTL-H30002 (v5.0 09/02/00 E)" region: [europe, oceania] group: ps-bios - required: true md5: "91c87cb2f2eb6ce529a2360f80ce2457" region_check: true fast_boot: Type2 @@ -506,7 +475,6 @@ files: description: "PS2, DTL-H30102 (v5.0 09/02/00 E)" region: [europe, oceania] group: ps-bios - required: true md5: "3016b3dd42148a67e2c048595ca4d7ce" region_check: true fast_boot: Type2 @@ -518,7 +486,6 @@ files: description: "PS2, SCPH-30002/SCPH-30003/SCPH-30004 (v5.0 09/02/00 E)" region: [europe, oceania] group: ps-bios - required: true md5: "b7fa11e87d51752a98b38e3e691cbf17" region_check: true fast_boot: Type2 @@ -530,7 +497,6 @@ files: description: "PS2, SCPH-18000 (GH-003) (v5.0 10/27/00 J)" region: [japan] group: ps-bios - required: true md5: "f63bc530bd7ad7c026fcd6f7bd0d9525" region_check: true fast_boot: Type2 @@ -542,7 +508,6 @@ files: description: "PS2, SCPH-18000 (GH-008) (v5.0 10/27/00 J)" region: [japan] group: ps-bios - required: true md5: "cee06bd68c333fc5768244eae77e4495" region_check: true fast_boot: Type2 @@ -554,7 +519,6 @@ files: description: "PS2, DTL-H30101 (v5.0 12/28/00 A)" region: [north-america, latin-america] group: ps-bios - required: true md5: "0bf988e9c7aaa4c051805b0fa6eb3387" region_check: true fast_boot: Type2 @@ -566,7 +530,6 @@ files: description: "PS2, SCPH-30001/SCPH-35001 (v5.0 12/28/00 A)" region: [north-america, latin-america] group: ps-bios - required: true md5: "8accc3c49ac45f5ae2c5db0adc854633" region_check: true fast_boot: Type2 @@ -578,7 +541,6 @@ files: description: "PS2, DTL-H30102 (v5.0 12/28/00 E)" region: [europe, oceania] group: ps-bios - required: true md5: "6f9a6feb749f0533aaae2cc45090b0ed" region_check: true fast_boot: Type2 @@ -590,7 +552,6 @@ files: description: "PS2, SCPH-30002/SCPH-30003/SCPH-30004/SCHP-35002/SCPH-35003/SCPH-35004 (v5.0 12/28/00 E)" region: [europe, oceania] group: ps-bios - required: true md5: "838544f12de9b0abc90811279ee223c8" region_check: true fast_boot: Type2 @@ -602,7 +563,6 @@ files: description: "PS2, DTL-H30000 (v5.0 01/18/01 J)" region: [japan] group: ps-bios - required: true md5: "bb6bbc850458fff08af30e969ffd0175" region_check: true fast_boot: Type2 @@ -614,7 +574,6 @@ files: description: "PS2, SCPH-30000/SCPH-35000 (v5.0 01/18/01 J)" region: [japan] group: ps-bios - required: true md5: "815ac991d8bc3b364696bead3457de7d" region_check: true fast_boot: Type2 @@ -626,7 +585,6 @@ files: description: "PS2, SCPH-30001R (v5.0 04/27/01 A)" region: [north-america, latin-america] group: ps-bios - required: true md5: "b107b5710042abe887c0f6175f6e94bb" region_check: true fast_boot: Type2 @@ -638,7 +596,6 @@ files: description: "PS2, SCPH-30000 (v5.0 04/27/01 J)" region: [japan] group: ps-bios - required: true md5: "ab55cceea548303c22c72570cfd4dd71" region_check: true fast_boot: Type2 @@ -650,7 +607,6 @@ files: description: "PS2, SCPH-30001R (v5.0 07/04/01 A)" region: [north-america, latin-america] group: ps-bios - required: true md5: "18bcaadb9ff74ed3add26cdf709fff2e" region_check: true fast_boot: Type2 @@ -662,7 +618,6 @@ files: description: "PS2, SCPH-30002R/SCPH-30003R/SCPH-30004R (v5.0 07/04/01 E)" region: [europe, oceania] group: ps-bios - required: true md5: "491209dd815ceee9de02dbbc408c06d6" region_check: true fast_boot: Type2 @@ -674,7 +629,6 @@ files: description: "PS2, SCPH-30001R (v5.0 10/04/01 A)" region: [north-america, latin-america] group: ps-bios - required: true md5: "7200a03d51cacc4c14fcdfdbc4898431" region_check: true fast_boot: Type2 @@ -686,7 +640,6 @@ files: description: "PS2, SCPH-30002R/SCPH-30003R/SCPH-30004R (v5.0 10/04/01 E)" region: [europe, oceania] group: ps-bios - required: true md5: "8359638e857c8bc18c3c18ac17d9cc3c" region_check: true fast_boot: Type2 @@ -698,7 +651,6 @@ files: description: "PS2, SCPH-30005R/SCPH-30006R/SCPH-30007R (v5.0 07/30/01 J)" region: [japan] group: ps-bios - required: true md5: "352d2ff9b3f68be7e6fa7e6dd8389346" region_check: true fast_boot: Type2 @@ -710,7 +662,6 @@ files: description: "PS2, SCPH-39001 (v5.0 02/07/02 A)" region: [north-america, latin-america] group: ps-bios - required: true md5: "d5ce2c7d119f563ce04bc04dbc3a323e" region_check: true fast_boot: Type2 @@ -722,7 +673,6 @@ files: description: "PS2, SCPH-39002/SCPH-39003/SCPH-39004 (v5.0 03/19/02 E)" region: [europe, oceania] group: ps-bios - required: true md5: "0d2228e6fd4fb639c9c39d077a9ec10c" region_check: true fast_boot: Type2 @@ -734,7 +684,6 @@ files: description: "PS2, SCPH-37000/SCPH-39000 (v5.0 04/26/02 J)" region: [japan] group: ps-bios - required: true md5: "72da56fccb8fcd77bba16d1b6f479914" region_check: true fast_boot: Type2 @@ -746,7 +695,6 @@ files: description: "PS2, SCPH-39008 (v5.0 04/26/02 E)" region: [europe, oceania] group: ps-bios - required: true md5: "5b1f47fbeb277c6be2fccdd6344ff2fd" region_check: true fast_boot: Type2 @@ -758,7 +706,6 @@ files: description: "PS2, SCPH-39005/SCPH-39006/SCPH-39007 (v5.0 04/26/02 J)" region: [japan] group: ps-bios - required: true md5: "315a4003535dfda689752cb25f24785c" region_check: true fast_boot: Type2 @@ -770,7 +717,6 @@ files: description: "PS2, DTL-H50000 (v5.0 02/06/03 J)" region: [japan] group: ps-bios - required: true md5: "54ecde087258557e2ddb5c3ddb004028" region_check: true fast_boot: Type2 @@ -782,7 +728,6 @@ files: description: "PS2, SCPH-50000/SCPH-55000 (v5.0 02/06/03 J)" region: [japan] group: ps-bios - required: true md5: "312ad4816c232a9606e56f946bc0678a" region_check: true fast_boot: Type2 @@ -794,7 +739,6 @@ files: description: "PS2, DTL-H50002 (v5.0 02/27/03 E)" region: [europe, oceania] group: ps-bios - required: true md5: "666018ffec65c5c7e04796081295c6c7" region_check: true fast_boot: Type2 @@ -806,7 +750,6 @@ files: description: "PS2, SCPH-50002/SCPH-50003/SCPH-50004 (v5.0 02/27/03 E)" region: [europe, oceania] group: ps-bios - required: true md5: "6e69920fa6eef8522a1d688a11e41bc6" region_check: true fast_boot: Type2 @@ -818,7 +761,6 @@ files: description: "PS2, DTL-H50001 (v5.0 03/25/03 A)" region: [north-america, latin-america] group: ps-bios - required: true md5: "eb960de68f0c0f7f9fa083e9f79d0360" region_check: true fast_boot: Type2 @@ -830,7 +772,6 @@ files: description: "PS2, SCPH-50001 (v5.0 03/25/03 A)" region: [north-america, latin-america] group: ps-bios - required: true md5: "8aa12ce243210128c5074552d3b86251" region_check: true fast_boot: Type2 @@ -842,7 +783,6 @@ files: description: "PS2, DTL-H50009 (v5.0 02/24/03 J)" region: [japan] group: ps-bios - required: true md5: "240d4c5ddd4b54069bdc4a3cd2faf99d" region_check: true fast_boot: Type2 @@ -854,7 +794,6 @@ files: description: "PS2, DESR-5000/DESR-5100/DESR-7000/DESR-7100 (v5.0 10/28/03 J)" region: [japan] group: ps-bios - required: true md5: "1c6cd089e6c83da618fbf2a081eb4888" region_check: true fast_boot: Type2 @@ -866,7 +805,6 @@ files: description: "PS2, SCPH-55000 (v5.0 06/23/03 J)" region: [japan] group: ps-bios - required: true md5: "463d87789c555a4a7604e97d7db545d1" region_check: true fast_boot: Type2 @@ -878,7 +816,6 @@ files: description: "PS2, DTL-H50001 (v5.0 06/23/03 A)" region: [north-america, latin-america] group: ps-bios - required: true md5: "ab9d49ad40ae49f19856ad187777b1b3" region_check: true fast_boot: Type2 @@ -890,7 +827,6 @@ files: description: "PS2, SCPH-50001/SCPH-50010 (v5.0 06/23/03 A)" region: [north-america, latin-america] group: ps-bios - required: true md5: "35461cecaa51712b300b2d6798825048" region_check: true fast_boot: Type2 @@ -902,7 +838,6 @@ files: description: "PS2, SCPH-50002/SCPH-50003/SCPH-50004 (v5.0 06/23/03 E)" region: [europe, oceania] group: ps-bios - required: true md5: "bd6415094e1ce9e05daabe85de807666" region_check: true fast_boot: Type2 @@ -914,7 +849,6 @@ files: description: "PS2, SCPH-50006/SCPH-50007 (v5.0 06/23/03 J)" region: [japan] group: ps-bios - required: true md5: "2e70ad008d4ec8549aada8002fdf42fb" region_check: true fast_boot: Type2 @@ -926,7 +860,6 @@ files: description: "PS2, SCPH-50005 (v5.0 06/23/03 J)" region: [japan] group: ps-bios - required: true md5: "50d5b97b57d8c9b6534adcb46c2027d4" region_check: true fast_boot: Type2 @@ -938,7 +871,6 @@ files: description: "PS2, SCPH-50008 (v5.0 06/23/03 E)" region: [europe, oceania] group: ps-bios - required: true md5: "b53d51edc7fc086685e31b811dc32aad" region_check: true fast_boot: Type2 @@ -950,7 +882,6 @@ files: description: "PS2, SCPH-50009 (v5.0 06/23/03 J)" region: [japan] group: ps-bios - required: true md5: "1b6e631b536247756287b916f9396872" region_check: true fast_boot: Type2 @@ -962,7 +893,6 @@ files: description: "PS2, SCPH-50000 (v5.0 08/22/03 J)" region: [japan] group: ps-bios - required: true md5: "00da1b177096cfd2532c8fa22b43e667" region_check: true fast_boot: Type2 @@ -974,7 +904,6 @@ files: description: "PS2, SCPH-50004 (v5.0 08/22/03 E)" region: [europe, oceania] group: ps-bios - required: true md5: "afde410bd026c16be605a1ae4bd651fd" region_check: true fast_boot: Type2 @@ -986,7 +915,6 @@ files: description: "PS2, SCPH-50011 (v5.0 03/29/04 A)" region: [north-america, latin-america] group: ps-bios - required: true md5: "81f4336c1de607dd0865011c0447052e" region_check: true fast_boot: Type2 @@ -998,7 +926,6 @@ files: description: "PS2, SCPH-70000 (v5.0 06/14/04 J)" region: [japan] group: ps-bios - required: true md5: "0eee5d1c779aa50e94edd168b4ebf42e" region_check: true fast_boot: Type2 @@ -1010,7 +937,6 @@ files: description: "PS2, SCPH-70001/SCPH-70011/SCPH-70012 (v5.0 06/14/04 A)" region: [north-america, latin-america] group: ps-bios - required: true md5: "d333558cc14561c1fdc334c75d5f37b7" region_check: true fast_boot: Type2 @@ -1022,7 +948,6 @@ files: description: "PS2, SCPH-70002/SCPH-70003/SCPH-70004/SCPH-70008 (v5.0 06/14/04 E)" region: [europe, oceania] group: ps-bios - required: true md5: "dc752f160044f2ed5fc1f4964db2a095" region_check: true fast_boot: Type2 @@ -1034,7 +959,6 @@ files: description: "PS2, SCPH-70002 (v5.0 06/14/04 E)" region: [europe, oceania] group: ps-bios - required: true md5: "7ebb4fc5eab6f79a27d76ac9aad392b2" region_check: true fast_boot: Type2 @@ -1046,7 +970,6 @@ files: description: "PS2, DTL-H70002 (v5.0 06/14/04 E)" region: [europe, oceania] group: ps-bios - required: true md5: "63ead1d74893bf7f36880af81f68a82d" region_check: true fast_boot: Type2 @@ -1058,7 +981,6 @@ files: description: "PS2, SCPH-70005/SCPH-70006/SCPH-70007 (v5.0 06/14/04 J)" region: [japan] group: ps-bios - required: true md5: "3e3e030c0f600442fa05b94f87a1e238" region_check: true fast_boot: Type2 @@ -1070,7 +992,6 @@ files: description: "PS2, DESR-5500/DESR-5700/DESR-7500/DESR-7700 (v5.0 09/17/04 J)" region: [japan] group: ps-bios - required: true md5: "1ad977bb539fc9448a08ab276a836bbc" region_check: true fast_boot: Type2 @@ -1082,7 +1003,6 @@ files: description: "PS2, DTL-H75000 (v5.0 06/20/05 J)" region: [japan] group: ps-bios - required: true md5: "bf0078ba5e19d57eae18047407f3b6e5" region_check: true fast_boot: Type2 @@ -1094,7 +1014,6 @@ files: description: "PS2, SCPH-75000 (v5.0 06/20/05 J)" region: [japan] group: ps-bios - required: true md5: "eb4f40fcf4911ede39c1bbfe91e7a89a" region_check: true fast_boot: Type2 @@ -1106,7 +1025,6 @@ files: description: "PS2, DTL-H75000A (v5.0 06/20/05 A)" region: [north-america, latin-america] group: ps-bios - required: true md5: "9959ad7a8685cad66206e7752ca23f8b" region_check: true fast_boot: Type2 @@ -1118,7 +1036,6 @@ files: description: "PS2, SCPH-75001/SCPH-75010 (v5.0 06/20/05 A)" region: [north-america, latin-america] group: ps-bios - required: true md5: "929a14baca1776b00869f983aa6e14d2" region_check: true fast_boot: Type2 @@ -1130,7 +1047,6 @@ files: description: "PS2, SCPH-75002/SCPH-75003/SCPH-75004/SCPH-75008 (v5.0 06/20/05 E)" region: [europe, oceania] group: ps-bios - required: true md5: "573f7d4a430c32b3cc0fd0c41e104bbd" region_check: true fast_boot: Type2 @@ -1142,7 +1058,6 @@ files: description: "PS2, SCPH-75006 (v5.0 06/20/05 J)" region: [japan] group: ps-bios - required: true md5: "df63a604e8bff5b0599bd1a6c2721bd0" region_check: true fast_boot: Type2 @@ -1154,7 +1069,6 @@ files: description: "PS2, SCPH-77000 (v5.0 02/10/06 J)" region: [japan] group: ps-bios - required: true md5: "5b1ba4bb914406fae75ab8e38901684d" region_check: true fast_boot: Type2 @@ -1166,7 +1080,6 @@ files: description: "PS2, SCPH-77001/SCPH-77010 (v5.0 02/10/06 A)" region: [north-america, latin-america] group: ps-bios - required: true md5: "cb801b7920a7d536ba07b6534d2433ca" region_check: true fast_boot: Type2 @@ -1178,7 +1091,6 @@ files: description: "PS2, SCPH-77002/SCPH-77003/SCPH-77004/SCPH-77008 (v5.0 02/10/06 E)" region: [europe, oceania] group: ps-bios - required: true md5: "af60e6d1a939019d55e5b330d24b1c25" region_check: true fast_boot: Type2 @@ -1190,7 +1102,6 @@ files: description: "PS2, SCPH-77006/SCPH-77007 (v5.0 02/10/06 J)" region: [japan] group: ps-bios - required: true md5: "549a66d0c698635ca9fa3ab012da7129" region_check: true fast_boot: Type2 @@ -1202,7 +1113,6 @@ files: description: "PS2, DTL-H90000 (v5.0 09/05/06 J)" region: [japan] group: ps-bios - required: true md5: "5e2014472c88f74f7547d8c2c60eca45" region_check: true fast_boot: Type2 @@ -1214,7 +1124,6 @@ files: description: "PS2, SCPH-79000/SCPH-90000 (v5.0 09/05/06 J)" region: [japan] group: ps-bios - required: true md5: "5de9d0d730ff1e7ad122806335332524" region_check: true fast_boot: Type2 @@ -1226,7 +1135,6 @@ files: description: "PS2, DTL-H90000 (v5.0 09/05/06 A)" region: [north-america, latin-america] group: ps-bios - required: true md5: "21fe4cad111f7dc0f9af29477057f88d" region_check: true fast_boot: Type2 @@ -1238,7 +1146,6 @@ files: description: "PS2, SCPH-79001/SCPH-79010/SCPH-90001 (v5.0 09/05/06 A)" region: [north-america, latin-america] group: ps-bios - required: true md5: "40c11c063b3b9409aa5e4058e984e30c" region_check: true fast_boot: Type2 @@ -1250,7 +1157,6 @@ files: description: "PS2, SCPH-79002/SCPH-79003/SCPH-79004/SCPH-79008/SCPH-90002/SCPH-90003/SCPH-90004 (v5.0 09/05/06 E)" region: [europe, oceania] group: ps-bios - required: true md5: "80bbb237a6af9c611df43b16b930b683" region_check: true fast_boot: Type2 @@ -1262,7 +1168,6 @@ files: description: "PS2, SCPH-79006/SCPH-79007/SCPH-90006/SCPH-90007 (v5.0 09/05/06 J)" region: [japan] group: ps-bios - required: true md5: "c37bce95d32b2be480f87dd32704e664" region_check: true fast_boot: Type2 @@ -1274,7 +1179,6 @@ files: description: "PS2, SCPH-90000 (v5.0 02/20/08 J)" region: [japan] group: ps-bios - required: true md5: "80ac46fa7e77b8ab4366e86948e54f83" region_check: true fast_boot: Type2 @@ -1286,7 +1190,6 @@ files: description: "PS2, SCPH-90001/SCPH-90010 (v5.0 02/20/08 A)" region: [north-america, latin-america] group: ps-bios - required: true md5: "21038400dc633070a78ad53090c53017" region_check: true fast_boot: Type2 @@ -1298,7 +1201,6 @@ files: description: "PS2, SCPH-90002/SCPH-90003/SCPH-90004/SCPH-90008 (v5.0 02/20/08 E)" region: [europe, oceania] group: ps-bios - required: true md5: "dc69f0643a3030aaa4797501b483d6c4" region_check: true fast_boot: Type2 @@ -1310,7 +1212,6 @@ files: description: "PS2, SCPH-90005/SCPH-90006/SCPH-90007 (v5.0 02/20/08 J)" region: [japan] group: ps-bios - required: true md5: "30d56e79d89fbddf10938fa67fe3f34e" region_check: true fast_boot: Type2 @@ -1322,7 +1223,6 @@ files: description: "PS2, KDL-22PX300 (v5.0 04/15/10 E)" region: [europe, oceania] group: ps-bios - required: true md5: "93ea3bcee4252627919175ff1b16a1d9" region_check: true fast_boot: Type2 @@ -1337,7 +1237,6 @@ files: description: "OpenBIOS" region: [world] group: ps-bios - required: true md5: null region_check: false fast_boot: unsupported diff --git a/emulators/dxx-rebirth.yml b/emulators/dxx-rebirth.yml index c61dec3d4..917f7760e 100644 --- a/emulators/dxx-rebirth.yml +++ b/emulators/dxx-rebirth.yml @@ -64,18 +64,23 @@ notes: | descent2 on install (scriptmodules/ports/dxx-rebirth.sh, game_data_dxx-rebirth). +groups: + d1-hog: + required: true + d2-hog: + required: true + files: # ------ Descent 1 shareware (free) ------ - name: descent.hog + group: d1-hog installed_on: [retropie] path: descent.hog system: descent category: game_data - required: false size: 2339773 validation: [size] description: "Descent 1 PC shareware v1.4 HOG" - variant_group: d1-hog note: "freely redistributable (icculus.org/d2x/data/desc14sw.tar.gz)" source_ref: "similar/main/mission.cpp:80; common/main/mission.h:67" @@ -90,14 +95,13 @@ files: # ------ Descent 1 retail ------ - name: descent.hog + group: d1-hog path: descent.hog system: descent category: game_data - required: false size: 6856701 validation: [size] description: "Descent 1 PC retail v1.5 HOG (GOG)" - variant_group: d1-hog note: "commercial, alternative sizes 6856183 (v1.5 alt), 7261423 (v1.0), 7456179 (Mac retail)" source_ref: "common/main/mission.h:61-64" @@ -111,15 +115,15 @@ files: # ------ Descent 2 shareware (free) ------ - name: d2demo.hog + group: d2-hog + priority: 2 installed_on: [retropie] path: d2demo.hog system: descent2 category: game_data - required: false size: 2292566 validation: [size] description: "Descent 2 PC shareware HOG" - variant_group: d2-hog note: "freely redistributable (icculus.org/d2x/data/d2shar10.tar.gz)" source_ref: "similar/main/mission.cpp:107-108; common/main/mission.h:71" @@ -143,14 +147,14 @@ files: # ------ Descent 2 retail ------ - name: descent2.hog + group: d2-hog + priority: 1 path: descent2.hog system: descent2 category: game_data - required: false size: 7595079 validation: [size] description: "Descent 2 PC full v1.1-1.2 HOG (GOG)" - variant_group: d2-hog note: "commercial, alternative sizes 7107354 (v1.0), 7110007 (Mac full), 6132957 (OEM v1.1), 4292746 (Mac shareware)" source_ref: "common/main/mission.h:73-76" diff --git a/emulators/epsxe.yml b/emulators/epsxe.yml index 44ee5dd1d..1598b88e8 100644 --- a/emulators/epsxe.yml +++ b/emulators/epsxe.yml @@ -133,13 +133,16 @@ notes: | ePSXe 2.0.18 Windows 0x4fb220-0x4fb263, DownloadShaderPluginTask.java:56-80 +groups: + ps-bios: + required: false + files: - name: scph1000.bin + group: ps-bios path: bios/scph1000.bin system: sony-playstation - required: false hle_fallback: true - variant_group: "psx-bios" config_key: "BiosPath" size: 524288 validation: [size, md5] @@ -149,11 +152,10 @@ files: source_ref: "ScanBiosTask.java:28, ScanBiosTask.java:162-172, ePSXe 2.0.18 Windows 0x5c10b4" - name: scph1001.bin + group: ps-bios path: bios/scph1001.bin system: sony-playstation - required: false hle_fallback: true - variant_group: "psx-bios" config_key: "BiosPath" size: 524288 validation: [size, md5] @@ -163,11 +165,10 @@ files: source_ref: "ScanBiosTask.java:28, ePSXe 2.0.5 Linux x64 write_configfiledefault 0x42ae99, ePSXeReadPreferences.java:21, ePSXe 2.0.18 Windows 0x5c10d8" - name: scph1001_v20.bin + group: ps-bios path: bios/scph1001_v20.bin system: sony-playstation - required: false hle_fallback: true - variant_group: "psx-bios" config_key: "BiosPath" size: 524288 validation: [size, md5] @@ -177,11 +178,10 @@ files: source_ref: "ScanBiosTask.java:28, ePSXe 2.0.18 Windows 0x5c1120" - name: scph1002a.bin + group: ps-bios path: bios/scph1002a.bin system: sony-playstation - required: false hle_fallback: true - variant_group: "psx-bios" config_key: "BiosPath" size: 524288 validation: [size, md5] @@ -191,11 +191,10 @@ files: source_ref: "ScanBiosTask.java:28, ePSXe 2.0.18 Windows 0x5c10fc" - name: scph3000.bin + group: ps-bios path: bios/scph3000.bin system: sony-playstation - required: false hle_fallback: true - variant_group: "psx-bios" config_key: "BiosPath" size: 524288 validation: [size, md5] @@ -205,11 +204,10 @@ files: source_ref: "ScanBiosTask.java:28, ePSXe 2.0.18 Windows 0x5c1144" - name: scph3500.bin + group: ps-bios path: bios/scph3500.bin system: sony-playstation - required: false hle_fallback: true - variant_group: "psx-bios" config_key: "BiosPath" size: 524288 validation: [size, md5] @@ -219,11 +217,10 @@ files: source_ref: "ScanBiosTask.java:28, ePSXe 2.0.18 Windows 0x5c1168" - name: dtlh1100.bin + group: ps-bios path: bios/dtlh1100.bin system: sony-playstation - required: false hle_fallback: true - variant_group: "psx-bios" config_key: "biosPref" size: 524288 validation: [size, md5] @@ -233,11 +230,10 @@ files: source_ref: "ScanBiosTask.java:28, ScanBiosTask.java:162-172" - name: scph1001_v21.bin + group: ps-bios path: bios/scph1001_v21.bin system: sony-playstation - required: false hle_fallback: true - variant_group: "psx-bios" config_key: "biosPref" size: 524288 validation: [size, md5] @@ -247,11 +243,10 @@ files: source_ref: "ScanBiosTask.java:28, ScanBiosTask.java:162-172" - name: scph1002b.bin + group: ps-bios path: bios/scph1002b.bin system: sony-playstation - required: false hle_fallback: true - variant_group: "psx-bios" config_key: "biosPref" size: 524288 validation: [size, md5] @@ -261,11 +256,10 @@ files: source_ref: "ScanBiosTask.java:28, ScanBiosTask.java:162-172" - name: scph5000.bin + group: ps-bios path: bios/scph5000.bin system: sony-playstation - required: false hle_fallback: true - variant_group: "psx-bios" config_key: "biosPref" size: 524288 validation: [size, md5] @@ -275,11 +269,10 @@ files: source_ref: "ScanBiosTask.java:28, ScanBiosTask.java:162-172" - name: scph1002c.bin + group: ps-bios path: bios/scph1002c.bin system: sony-playstation - required: false hle_fallback: true - variant_group: "psx-bios" config_key: "BiosPath" size: 524288 validation: [size, md5] @@ -289,11 +282,10 @@ files: source_ref: "ScanBiosTask.java:28, ePSXe 2.0.18 Windows 0x5c1090" - name: scph5500.bin + group: ps-bios path: bios/scph5500.bin system: sony-playstation - required: false hle_fallback: true - variant_group: "psx-bios" config_key: "BiosPath" size: 524288 validation: [size, md5] @@ -303,11 +295,10 @@ files: source_ref: "ScanBiosTask.java:28, ePSXe 2.0.18 Windows 0x5c11b0" - name: scph5501.bin + group: ps-bios path: bios/scph5501.bin system: sony-playstation - required: false hle_fallback: true - variant_group: "psx-bios" config_key: "BiosPath" size: 524288 validation: [size, md5] @@ -317,11 +308,10 @@ files: source_ref: "ScanBiosTask.java:28, ePSXe 2.0.18 Windows 0x5c1288" - name: scph5502.bin + group: ps-bios path: bios/scph5502.bin system: sony-playstation - required: false hle_fallback: true - variant_group: "psx-bios" config_key: "BiosPath" size: 524288 validation: [size, md5] @@ -331,11 +321,10 @@ files: source_ref: "ScanBiosTask.java:28, ePSXe 2.0.18 Windows 0x5c11f8" - name: scph7000.bin + group: ps-bios path: bios/scph7000.bin system: sony-playstation - required: false hle_fallback: true - variant_group: "psx-bios" config_key: "BiosPath" size: 524288 validation: [size, md5] @@ -345,11 +334,10 @@ files: source_ref: "ScanBiosTask.java:28, ePSXe 2.0.18 Windows 0x5c121c" - name: scph7001.bin + group: ps-bios path: bios/scph7001.bin system: sony-playstation - required: false hle_fallback: true - variant_group: "psx-bios" config_key: "BiosPath" size: 524288 validation: [size, md5] @@ -359,11 +347,10 @@ files: source_ref: "ScanBiosTask.java:28, ePSXe 2.0.18 Windows 0x5c1240" - name: scph7002.bin + group: ps-bios path: bios/scph7002.bin system: sony-playstation - required: false hle_fallback: true - variant_group: "psx-bios" config_key: "BiosPath" size: 524288 validation: [size, md5] @@ -373,11 +360,10 @@ files: source_ref: "ScanBiosTask.java:28, ePSXe 2.0.18 Windows 0x5c1264, ePSXe 2.0.18 Windows 0x5c12ac, ePSXe 2.0.18 Windows 0x5c12d0" - name: scph100.bin + group: ps-bios path: bios/scph100.bin system: sony-playstation - required: false hle_fallback: true - variant_group: "psx-bios" config_key: "BiosPath" size: 524288 validation: [size, md5] @@ -387,11 +373,10 @@ files: source_ref: "ScanBiosTask.java:28, ePSXe 2.0.18 Windows 0x5c12f4" - name: scph102_v44.bin + group: ps-bios path: bios/scph102_v44.bin system: sony-playstation - required: false hle_fallback: true - variant_group: "psx-bios" config_key: "BiosPath" size: 524288 validation: [size, md5] @@ -401,11 +386,10 @@ files: source_ref: "ScanBiosTask.java:28, ePSXe 2.0.18 Windows 0x5c133c" - name: scph101_v45.bin + group: ps-bios path: bios/scph101_v45.bin system: sony-playstation - required: false hle_fallback: true - variant_group: "psx-bios" config_key: "BiosPath" size: 524288 validation: [size, md5] @@ -415,11 +399,10 @@ files: source_ref: "ScanBiosTask.java:28, ePSXe 2.0.18 Windows 0x5c1318" - name: scph102_v45.bin + group: ps-bios path: bios/scph102_v45.bin system: sony-playstation - required: false hle_fallback: true - variant_group: "psx-bios" config_key: "BiosPath" size: 524288 validation: [size, md5] @@ -429,11 +412,10 @@ files: source_ref: "ScanBiosTask.java:28, ePSXe 2.0.18 Windows 0x5c1360" - name: scph5000_b.bin + group: ps-bios path: bios/scph5000_b.bin system: sony-playstation - required: false hle_fallback: true - variant_group: "psx-bios" config_key: "BiosPath" size: 524288 validation: [size] @@ -443,11 +425,10 @@ files: source_ref: "ePSXe 2.0.18 Windows 0x5c118c, ePSXe 2.0.18 Windows 0x42f525-0x42f568" - name: scph5502_b.bin + group: ps-bios path: bios/scph5502_b.bin system: sony-playstation - required: false hle_fallback: true - variant_group: "psx-bios" config_key: "BiosPath" size: 524288 validation: [size] diff --git a/emulators/fpse-ng.yml b/emulators/fpse-ng.yml index acff63142..9cbf99479 100644 --- a/emulators/fpse-ng.yml +++ b/emulators/fpse-ng.yml @@ -133,13 +133,16 @@ notes: | Main.java:11014, Main.java:12555-12585, Main.java:15415-15430, Main.java:15489 +groups: + ps-bios: + required: false + files: - name: scph1001.bin + group: ps-bios path: bios/scph1001.bin system: sony-playstation - required: false hle_fallback: true - variant_group: "psx-bios" config_key: "com.emulator.fpse64.biospath" size: 524288 validation: [size] diff --git a/emulators/fpse.yml b/emulators/fpse.yml index 6ff069202..d395957e1 100644 --- a/emulators/fpse.yml +++ b/emulators/fpse.yml @@ -105,13 +105,16 @@ notes: | ref: Main.java:26892-26895, Main.java:26956-26957, Main.java:29721, Main.java:29755-29758, Main.java:30046 +groups: + ps-bios: + required: false + files: - name: scph1001.bin + group: ps-bios path: bios/scph1001.bin system: sony-playstation - required: false hle_fallback: true - variant_group: "psx-bios" config_key: "BiosName" size: 524288 validation: [size] diff --git a/emulators/freechaf.yml b/emulators/freechaf.yml index 5b8681430..a094a1a8d 100644 --- a/emulators/freechaf.yml +++ b/emulators/freechaf.yml @@ -37,12 +37,15 @@ notes: | The .info file declares all three firmware files as required (opt=false), but the core does not abort on a missing BIOS. It falls back to HLE. +groups: + psu1: + required: false + files: - name: "sl31253.bin" system: fairchild-channelf group: psu1 priority: 2 - required: false hle_fallback: true note: "Channel F PSU1 ROM (original). Fallback if sl90025.bin is missing." source_ref: "src/libretro.c:192" @@ -58,7 +61,6 @@ files: system: fairchild-channelf group: psu1 priority: 1 - required: false hle_fallback: true note: "Channel F II PSU1 ROM. Tried first, preferred over sl31253.bin." source_ref: "src/libretro.c:186" diff --git a/emulators/kronos.yml b/emulators/kronos.yml index 43b4fb0f4..8090a1923 100644 --- a/emulators/kronos.yml +++ b/emulators/kronos.yml @@ -48,6 +48,10 @@ notes: | archive_prefix: kronos +groups: + saturn-bios: + required: true + files: # ----------------------------------------------------------- # Saturn BIOS - primary (any region) @@ -57,7 +61,6 @@ files: system: sega-saturn group: saturn-bios priority: 1 - required: true size: 524288 validation: [size] note: "Primary Saturn BIOS. Any region accepted. Searched in kronos/ subdir first." @@ -67,7 +70,6 @@ files: system: sega-saturn group: saturn-bios priority: 2 - required: true size: 524288 validation: [size] note: "The same name in the system directory, read when kronos/ holds none." @@ -80,7 +82,6 @@ files: system: sega-saturn group: saturn-bios priority: 3 - required: true size: 524288 validation: [size] note: "Saturn BIOS v1.01 (Japan). Fallback if saturn_bios.bin missing." @@ -93,7 +94,6 @@ files: system: sega-saturn group: saturn-bios priority: 4 - required: true size: 524288 validation: [size] note: "Saturn BIOS (NA/EU). Last fallback for Saturn mode." diff --git a/emulators/numero.yml b/emulators/numero.yml index d750e5ab5..3fa84dbf7 100644 --- a/emulators/numero.yml +++ b/emulators/numero.yml @@ -39,13 +39,16 @@ notes: | underlying engine (calc.cpp:rom_load) auto-detects the model from the ROM header. +groups: + rom: + required: true + files: - name: "ti83se.rom" system: ti-83 description: "TI-83 Plus Silver Edition ROM dump" group: rom priority: 1 - required: true source_ref: "source libnumero/libretro/libretronew.cpp:576-578" note: "Checked first. Recommended for largest storage capacity." @@ -54,7 +57,6 @@ files: description: "TI-83 Plus ROM dump" group: rom priority: 2 - required: true source_ref: "source libnumero/libretro/libretronew.cpp:583-585" note: "Checked second, used if ti83se.rom is missing." @@ -63,6 +65,5 @@ files: description: "TI-83 ROM dump" group: rom priority: 3 - required: true source_ref: "source libnumero/libretro/libretronew.cpp:591-593" note: "Checked last. Original TI-83 (non-Plus)." diff --git a/emulators/o2em.yml b/emulators/o2em.yml index 586ec8cb9..2ebe47e47 100644 --- a/emulators/o2em.yml +++ b/emulators/o2em.yml @@ -41,7 +41,7 @@ files: - name: "o2rom.bin" system: odyssey2 required: true - variant_group: "o2em-bios" + selection_slot: "o2em-bios" min_size: 1024 md5: "562d5ebf9e030a40d6fabfc2f33139fd" sha1: "b2e1955d957a475de2411770452eff4ea19f4cee" @@ -57,7 +57,7 @@ files: - name: "c52.bin" system: videopac required: false - variant_group: "o2em-bios" + selection_slot: "o2em-bios" min_size: 1024 md5: "f1071cdb0b6b10dde94d3bc8a6146387" sha1: "a6120aed50831c9c0d95dbdf707820f601d9452e" @@ -69,7 +69,7 @@ files: - name: "g7400.bin" system: videopac required: false - variant_group: "o2em-bios" + selection_slot: "o2em-bios" min_size: 1024 md5: "c500ff71236068e0dc0d0603d265ae76" sha1: "5130243429b40b01a14e1304d0394b8459a6fbae" @@ -81,7 +81,7 @@ files: - name: "jopac.bin" system: videopac required: false - variant_group: "o2em-bios" + selection_slot: "o2em-bios" min_size: 1024 md5: "279008e4a0db2dc5f1c048853b033828" sha1: "54b8d2c1317628de51a85fc1c424423a986775e4" diff --git a/emulators/odin.yml b/emulators/odin.yml index bfdf1ac9c..04217ef3b 100644 --- a/emulators/odin.yml +++ b/emulators/odin.yml @@ -71,6 +71,10 @@ notes: | archive_prefix: "odin" +groups: + boot-rom: + required: true + files: - name: "hikaru.zip" path: "odin/hikaru.zip" @@ -84,8 +88,6 @@ files: archive: hikaru.zip system: sega-hikaru group: boot-rom - required: true - variant_group: "hikaru-bootrom" priority: 1 size: 2097152 crc32: "2aa906a7" @@ -97,8 +99,6 @@ files: archive: hikaru.zip system: sega-hikaru group: boot-rom - required: true - variant_group: "hikaru-bootrom" priority: 2 size: 2097152 crc32: "3d557104" @@ -110,8 +110,6 @@ files: archive: hikaru.zip system: sega-hikaru group: boot-rom - required: true - variant_group: "hikaru-bootrom" priority: 3 size: 2097152 crc32: "d96298b6" diff --git a/emulators/pcsx1.yml b/emulators/pcsx1.yml index bc4c8fbf2..8b0afff7e 100644 --- a/emulators/pcsx1.yml +++ b/emulators/pcsx1.yml @@ -22,12 +22,15 @@ notes: > No region detection, no hash validation. First valid BIOS found is used regardless of region. .info declares scph5500/5501/5502 but code explicitly searches scph1001/5501/7001 first. +groups: + bios: + required: false + files: - name: "scph1001.bin" description: "SCPH-1001 (v2.2 12-04-95 A)" group: bios priority: 1 - required: false hle_fallback: true size: 524288 validation: [size] @@ -40,7 +43,6 @@ files: description: "SCPH-5501 (v3.0 11-18-96 A)" group: bios priority: 2 - required: false hle_fallback: true size: 524288 validation: [size] @@ -53,7 +55,6 @@ files: description: "SCPH-7001 (v4.1 12-16-97 A)" group: bios priority: 3 - required: false hle_fallback: true size: 524288 validation: [size] diff --git a/emulators/picodrive.yml b/emulators/picodrive.yml index 17d67015d..30631927b 100644 --- a/emulators/picodrive.yml +++ b/emulators/picodrive.yml @@ -55,6 +55,14 @@ notes: | initializes VDP registers and RAM to simulate post-BIOS state (pico/sms.c:1080-1097). +groups: + mega-cd-us: + required: true + mega-cd-eu: + required: true + mega-cd-jp: + required: true + files: # ------------------------------------------------------- # Cartridge hardware database @@ -69,44 +77,40 @@ files: # ------------------------------------------------------- - name: "us_scd2_9306.bin" system: sega-megacd - variant_group: "mega-cd-bios" + selection_slot: "mega-cd-bios" region: [north-america, latin-america] group: mega-cd-us priority: 1 - required: true size: 131072 # 128 KB (0x20000) note: "US Sega CD Model 2 BIOS (September 1993). First in US search order. The US list serves every disc whose security byte is neither 0x64 nor 0xa1 (pico/cd/cd_image.c:190, 247-248)." source_ref: "source platform/libretro/libretro.c:1273" - name: "SegaCDBIOS9303.bin" system: sega-megacd - variant_group: "mega-cd-bios" + selection_slot: "mega-cd-bios" region: [north-america, latin-america] group: mega-cd-us priority: 2 - required: true size: 131072 note: "US Sega CD BIOS (March 1993). Second in US search order." source_ref: "source platform/libretro/libretro.c:1273" - name: "us_scd1_9210.bin" system: sega-megacd - variant_group: "mega-cd-bios" + selection_slot: "mega-cd-bios" region: [north-america, latin-america] group: mega-cd-us priority: 3 - required: true size: 131072 note: "US Sega CD Model 1 BIOS (October 1992). Third in US search order." source_ref: "source platform/libretro/libretro.c:1273" - name: "bios_CD_U.bin" system: sega-megacd - variant_group: "mega-cd-bios" + selection_slot: "mega-cd-bios" region: [north-america, latin-america] group: mega-cd-us priority: 4 - required: true size: 131072 note: "US Sega CD BIOS (generic name). Last in US search order." source_ref: "source platform/libretro/libretro.c:1273" @@ -116,44 +120,40 @@ files: # ------------------------------------------------------- - name: "eu_mcd2_9306.bin" system: sega-megacd - variant_group: "mega-cd-bios" + selection_slot: "mega-cd-bios" region: [europe, oceania] group: mega-cd-eu priority: 1 - required: true size: 131072 note: "EU Mega CD Model 2 BIOS (June 1993). First in EU search order, used when security byte 0x20b is 0x64, the only PAL branch for Australian discs (pico/media.c:243-247, platform/libretro/libretro.c:1306-1308)." source_ref: "source platform/libretro/libretro.c:1276" - name: "eu_mcd2_9303.bin" system: sega-megacd - variant_group: "mega-cd-bios" + selection_slot: "mega-cd-bios" region: [europe, oceania] group: mega-cd-eu priority: 2 - required: true size: 131072 note: "EU Mega CD Model 2 BIOS (March 1993). Second in EU search order." source_ref: "source platform/libretro/libretro.c:1276" - name: "eu_mcd1_9210.bin" system: sega-megacd - variant_group: "mega-cd-bios" + selection_slot: "mega-cd-bios" region: [europe, oceania] group: mega-cd-eu priority: 3 - required: true size: 131072 note: "EU Mega CD Model 1 BIOS (October 1992). Third in EU search order." source_ref: "source platform/libretro/libretro.c:1276" - name: "bios_CD_E.bin" system: sega-megacd - variant_group: "mega-cd-bios" + selection_slot: "mega-cd-bios" region: [europe, oceania] group: mega-cd-eu priority: 4 - required: true size: 131072 note: "EU Mega CD BIOS (generic name). Last in EU search order." source_ref: "source platform/libretro/libretro.c:1276" @@ -163,44 +163,40 @@ files: # ------------------------------------------------------- - name: "jp_mcd2_921222.bin" system: sega-megacd - variant_group: "mega-cd-bios" + selection_slot: "mega-cd-bios" region: [japan] group: mega-cd-jp priority: 1 - required: true size: 131072 note: "JP Mega CD Model 2 BIOS (December 1992). First in JP search order." source_ref: "source platform/libretro/libretro.c:1279" - name: "jp_mcd1_9112.bin" system: sega-megacd - variant_group: "mega-cd-bios" + selection_slot: "mega-cd-bios" region: [japan] group: mega-cd-jp priority: 2 - required: true size: 131072 note: "JP Mega CD Model 1 BIOS (December 1991). Second in JP search order." source_ref: "source platform/libretro/libretro.c:1279" - name: "jp_mcd1_9111.bin" system: sega-megacd - variant_group: "mega-cd-bios" + selection_slot: "mega-cd-bios" region: [japan] group: mega-cd-jp priority: 3 - required: true size: 131072 note: "JP Mega CD Model 1 BIOS (November 1991). Third in JP search order." source_ref: "source platform/libretro/libretro.c:1279" - name: "bios_CD_J.bin" system: sega-megacd - variant_group: "mega-cd-bios" + selection_slot: "mega-cd-bios" region: [japan] group: mega-cd-jp priority: 4 - required: true size: 131072 note: "JP Mega CD BIOS (generic name). Last in JP search order." source_ref: "source platform/libretro/libretro.c:1279" diff --git a/emulators/pizza-boy-sc.yml b/emulators/pizza-boy-sc.yml index b1c7d787b..734be75ef 100644 --- a/emulators/pizza-boy-sc.yml +++ b/emulators/pizza-boy-sc.yml @@ -90,7 +90,7 @@ files: system: sega-megacd required: true agnostic: true - variant_group: "megacd-boot-rom" + selection_slot: "megacd-boot-rom" size: 131072 validation: [size] description: "Sega CD boot ROM" @@ -116,7 +116,7 @@ files: system: sega-megacd required: true agnostic: true - variant_group: "megacd-boot-rom" + selection_slot: "megacd-boot-rom" size: 131072 validation: [size] description: "Mega CD boot ROM" @@ -129,7 +129,7 @@ files: system: sega-megacd required: true agnostic: true - variant_group: "megacd-boot-rom" + selection_slot: "megacd-boot-rom" size: 131072 validation: [size] description: "Mega CD boot ROM (Japan)" @@ -142,7 +142,7 @@ files: system: sega-mastersystem required: false agnostic: true - variant_group: "mastersystem-boot-rom" + selection_slot: "mastersystem-boot-rom" description: "Master System boot ROM" note: >- Picked and stored like the Mega CD image and handed to the machine as a path. @@ -157,7 +157,7 @@ files: system: sega-mastersystem required: false agnostic: true - variant_group: "mastersystem-boot-rom" + selection_slot: "mastersystem-boot-rom" description: "Master System boot ROM (Europe)" note: "Same slot and same absence of tests as the other Master System boot ROMs." source_ref: "Pizza Boy SC Basic 1.2.14 libsc.so frontend/android/src/lib.rs:590, @@ -167,7 +167,7 @@ files: system: sega-mastersystem required: false agnostic: true - variant_group: "mastersystem-boot-rom" + selection_slot: "mastersystem-boot-rom" description: "Master System boot ROM (Japan)" note: "Same slot and same absence of tests as the other Master System boot ROMs." source_ref: "Pizza Boy SC Basic 1.2.14 libsc.so frontend/android/src/lib.rs:590, diff --git a/emulators/pom2.yml b/emulators/pom2.yml index 00fdee198..683c0d773 100644 --- a/emulators/pom2.yml +++ b/emulators/pom2.yml @@ -61,8 +61,10 @@ notes: | //c apple2c-32Kv0.rom, apple2c-16K.rom, 3420033a.256 //c Plus apple2cp.rom, apple2c-plus.rom, apple2c-32Kv0.rom With no candidate the machine runs on a $D000-$FFFF stub and the Welcome - panel opens (src/MainWindow.cpp:357-364). A first run with no //e image - falls back to the ][+ chain (src/MainWindow.cpp:246-259). + panel opens (src/MainWindow.cpp:357-364). A first run boots the //e on + apple2e.rom, else the ][+ chain, else the //c chain + (src/MainWindow.cpp:246-296): one of these is the set the machine needs, + each model's chain is a set of its own. Memory::loadAppleIIRom (src/Memory.cpp:177-264) maps 12 KB at $D000, 16 KB at $C000, skips the 4 KB pad of a 20 KB ][+ dump, and slices a 32 KB dump by profile: //e images carry the firmware in the upper half, @@ -190,13 +192,32 @@ notes: | browsed from disks_5.4/, disks_3.5/, hdv/, floppyemu/, bundled_disks/, cassettes/ and prodos_folder/ next to the executable. +groups: + boot: + required: true + ii: + required: false + ii-plus: + required: false + iie-unenhanced: + required: false + iic: + required: false + iic-plus: + required: false + char: + required: false + char-iie-unenhanced: + required: false + videoterm: + required: false + files: - name: "apple2o.rom" path: "roms/apple2o.rom" + group: {ii: 1} description: "Apple ][ Original monitor and Integer BASIC, $D000-$FFFF" - required: false bundled: false - priority: 1 size: 12288 sha256: "68d9db6bb4c305d40c3fa89fa0f2d7b7f71516a9431e3138859f23bc5bddb2d1" sha1: "78008be557f643e956a106121bcc182c0fb9ea6d" @@ -205,10 +226,9 @@ files: note: "First candidate of the Apple ][ Original profile. apple2.rom is probed next." - name: "apple2.rom" path: "roms/apple2.rom" + group: {ii: 2, ii-plus: 2, boot: 3} description: "Apple ][ generic 12 KB fallback, Autostart monitor and Applesoft" - required: false bundled: false - priority: 2 size: 12288 sha256: "fc3e9d41e9428534a883df5aa10eb55b73ea53d2fcbb3ee4f39bed1b07a82905" sha1: "33a24f5489ba9195b44be77d9afb2252594cb5c7" @@ -217,10 +237,9 @@ files: note: "Probed after the profile image for the ][ and ][+ profiles. When it resolves in place of that image the loader logs that the ROM identity may not match the machine." - name: "apple2p.rom" path: "roms/apple2p.rom" + group: {ii-plus: 1, boot: 2} description: "Apple ][+ Autostart monitor and Applesoft, 4 KB pad then $C000-$FFFF" - required: false bundled: false - priority: 1 size: [12288, 20480] sha256: "92c4bef609920842ea472d21b661a0d35dbda6cd90963b8b734a205e22d84108" sha1: "29a53f3bb158b160433369e8e4a1d7cd5bf68ac6" @@ -229,10 +248,9 @@ files: note: "First candidate of the ][+ profile and of the first-run fallback (src/MainWindow.cpp:255-259). The loader drops the leading 4 KB of the 20 KB dump. The fetch gate names that dump, but a 12 KB six-chip image already on disk is kept as present without a digest check." - name: "apple2e.rom" path: "roms/apple2e.rom" + group: {boot: 1, iie-unenhanced: 3} description: "Apple //e Enhanced firmware, 32 KB dump with the 16 KB firmware in the upper half" - required: true bundled: false - priority: 1 size: 32768 sha256: "c17bc38c75ba96c33a30c688a1efd60144811073423533fe7f8453cdd9457aab" sha1: "d756eba578a6c5a4807510dfee30ee585c63755e" @@ -241,10 +259,9 @@ files: note: "Sole candidate of the //e Enhanced profiles, NTSC and PAL, PAL being the default machine. Also the last fallback of the //e Unenhanced chain." - name: "apple2e_unenh.rom" path: "roms/apple2e_unenh.rom" + group: {iie-unenhanced: 1} description: "Apple //e Unenhanced firmware, 4 KB internal $Cx00 ROM then $D000-$FFFF" - required: false bundled: false - priority: 1 aliases: ["342-0135-b.64.rom"] size: 16384 sha256: "1fb812584c6633fa16b77b20915986ed1178d1e6fc07a647f7ee8d4e6ab9d40b" @@ -254,10 +271,9 @@ files: note: "First candidate of the //e Unenhanced profiles. 342-0135-b.64.rom is probed second, apple2e.rom third. The fetcher assembles it from the two 8 KB chips of the MAME apple2e set, 342-0135-b.64 then 342-0134-a.64." - name: "apple2c-32Kv0.rom" path: "roms/apple2c-32Kv0.rom" + group: {iic: 1, iic-plus: 3, boot: 4} description: "Apple //c two-bank firmware, bank 0 in the lower 16 KB" - required: false bundled: false - priority: 1 size: 32768 crc32: "323952d1" sha256: "d65bbf97058b9cdce8eaebf74124048e3b4e42dca1bd42be316747bbc27dc124" @@ -267,10 +283,9 @@ files: note: "First candidate of the //c and //c PAL profiles, third of the //c Plus chain. Bank 0 is mapped at reset, the $C028 ROMBANK switch selects the other. Identical to 3420033a.256 with its first 256 bytes zeroed." - name: "apple2c-16K.rom" path: "roms/apple2c-16K.rom" + group: {iic: 2, boot: 5} description: "Apple //c ROM 255, 16 KB, MAME apple2c a2c.128" - required: false bundled: false - priority: 2 size: 16384 crc32: "f0edaa1b" sha256: "3ca1c8a27bf111aa0f4dc9372c5e6404457adb9a458ce32ae4b03a7e989e0c76" @@ -280,10 +295,9 @@ files: note: "Second candidate of the //c chain, loaded at $C000." - name: "3420033a.256" path: "roms/3420033a.256" + group: {iic: 3, boot: 6} description: "Apple //c ROM 0, UniDisk 3.5 firmware, MAME apple2c0 342-0033-A" - required: false bundled: false - priority: 3 size: 32768 crc32: "c8b979b3" sha256: "678d5c9d374664d2ee94ba9c9564d26a917b31116b07c46df30a3e82c640885b" @@ -293,10 +307,9 @@ files: note: "Third candidate of the //c chain." - name: "apple2cp.rom" path: "roms/apple2cp.rom" + group: {iic-plus: 1} description: "Apple //c Plus two-bank firmware, MAME apple2cp 341-0625-a.256" - required: false bundled: false - priority: 1 aliases: ["apple2c-plus.rom"] size: 32768 crc32: "0b996420" @@ -307,10 +320,9 @@ files: note: "First candidate of the //c Plus profile. apple2c-plus.rom is probed second and apple2c-32Kv0.rom third." - name: "apple2_char.rom" path: "roms/apple2_char.rom" + group: {char: 2, char-iie-unenhanced: 3} description: "Apple ][ and ][+ character generator, 2 KB uppercase set" - required: false bundled: false - priority: 1 size: 2048 sha256: "08f5d22230481019844492dde0a29a018cb193712a9e4a43770a3870608f28de" sha1: "f9d312f128c9557d9d6ac03bfad6c3ddf83e5659" @@ -319,10 +331,9 @@ files: note: "Sole candidate of the ][ and ][+ profiles and the last fallback of every //e-class chain." - name: "apple2e_char.rom" path: "roms/apple2e_char.rom" + group: {char: 1} description: "Apple //e Enhanced character generator 342-0265-A, 4 KB with MouseText" - required: false bundled: false - priority: 1 aliases: ["apple2e_char_us.rom"] size: 4096 sha256: "52c3b87900ac939f6525402cab1ccfd8f8259290fc6df54da48fb4c98ae3ed0f" @@ -332,10 +343,9 @@ files: note: "First candidate of the //e Enhanced, //c and //c Plus profiles. The same dump answers to apple2e_char_us.rom in the locale picker, and the fetcher writes both names." - name: "apple2e_char_us_unenh.rom" path: "roms/apple2e_char_us_unenh.rom" + group: {char-iie-unenhanced: 1} description: "Apple //e Unenhanced character generator 342-0133-A, 4 KB" - required: false bundled: false - priority: 1 aliases: ["apple2e_char_2k.rom", "342-0133-a.chr.rom"] size: 4096 sha256: "ed5bdd4afa509134e85f1d020685af7ff50e279226eb869a17825b471cc1634c" @@ -629,9 +639,8 @@ files: note: "The Apple Parallel Interface leaves its slot empty without it. The fetcher extracts it from the MAME a2pic set as 341-0057.7b." - name: "videx_videoterm_v24_60hz.bin" path: "roms/videx_videoterm_v24_60hz.bin" + group: {videoterm: 1} description: "Videx Videoterm firmware v2.4 with the 60 Hz CRTC table, 2 KB, MAME a2videoterm 6.ic6.bin" - required: false - priority: 1 size: 2048 crc32: "5776fa24" sha256: "420da3867e489824f5fc0377c287c48a125296f19e1306df764e8f63bd23b00a" @@ -641,9 +650,8 @@ files: note: "First firmware candidate of the Videoterm card. The 50 Hz dump is probed second. With neither the slot stays empty. The fetcher extracts it from the MAME a2vidtrm set." - name: "videx_videoterm_v24_50hz.bin" path: "roms/videx_videoterm_v24_50hz.bin" + group: {videoterm: 2} description: "Videx Videoterm firmware v2.4 with the 50 Hz CRTC table, 1 KB, MAME a2videoterm videx videoterm rom 2.4.bin" - required: false - priority: 2 size: 1024 crc32: "bbe3bb28" sha256: "616f4d9f81a7a4ea0fa918842a05dfb3b503927776d2bfe97e6950b5431b562e" diff --git a/emulators/px68k-sdl.yml b/emulators/px68k-sdl.yml index eeee0254f..33214e6c3 100644 --- a/emulators/px68k-sdl.yml +++ b/emulators/px68k-sdl.yml @@ -47,13 +47,18 @@ notes: | General MIDI SoundFont works. The Mercury unit is compiled out (Makefile:120-123). +groups: + ipl: + required: true + font: + required: true + files: - name: "iplrom.dat" system: sharp-x68000 path: "iplrom.dat" group: ipl priority: 1 - required: true size: 131072 source_ref: "SDL/SDL2/winx68k.cpp:277-279,288-307" note: > @@ -67,7 +72,6 @@ files: path: "iplromxv.dat" group: ipl priority: 2 - required: true size: 131072 source_ref: "SDL/SDL2/winx68k.cpp:277-279,288-307" note: "X68000 XVI IPL ROM, read when iplrom.dat is absent." @@ -77,7 +81,6 @@ files: path: "iplromco.dat" group: ipl priority: 3 - required: true size: 131072 source_ref: "SDL/SDL2/winx68k.cpp:277-279,288-307" note: "X68000 Compact IPL ROM, read when the first two names are absent." @@ -87,7 +90,6 @@ files: path: "iplrom30.dat" group: ipl priority: 4 - required: true size: 131072 source_ref: "SDL/SDL2/winx68k.cpp:277-279,288-307" note: "X68030 IPL ROM, read when the three other names are absent." @@ -97,7 +99,6 @@ files: path: "cgrom30.dat" group: font priority: 1 - required: true size: 786432 source_ref: "SDL/SDL2/winx68k.cpp:280-282,317-331" note: > @@ -110,7 +111,6 @@ files: path: "cgrom30.tmp" group: font priority: 2 - required: true unsourceable: "rendered from TrueType fonts by the font generator, no canonical file" size: 786432 source_ref: "SDL/SDL2/winx68k.cpp:280-282,317-331" @@ -121,7 +121,6 @@ files: path: "cgrom.dat" group: font priority: 3 - required: true size: 786432 source_ref: "SDL/SDL2/winx68k.cpp:280-282,317-331" note: "X68000 character generator ROM, read when neither X68030 font is present." @@ -131,7 +130,6 @@ files: path: "cgrom.tmp" group: font priority: 4 - required: true unsourceable: "rendered from TrueType fonts by the font generator, no canonical file" size: 786432 source_ref: "SDL/SDL2/winx68k.cpp:280-282,317-331, SDL/tool/create_cgrom.c:20" diff --git a/emulators/px68k.yml b/emulators/px68k.yml index 1f0bbe529..0117fc6f9 100644 --- a/emulators/px68k.yml +++ b/emulators/px68k.yml @@ -40,6 +40,12 @@ systems: # /keropi/sram.dat (auto-created) # /keropi/config (auto-created, core settings) +groups: + ipl: + required: true + font: + required: true + files: # IPL ROM, the main system BIOS. The core tries four filenames in order and # uses the first one found. Only one is needed. iplrom.dat is the original X68000, @@ -51,7 +57,6 @@ files: group: ipl priority: 1 size: 131072 - required: true note: > X68000 IPL ROM (original model). 128 KB, loaded at IPL buffer offset 0x20000. The core tries iplrom.dat first, then iplrom30.dat, iplromco.dat, iplromxv.dat @@ -63,7 +68,6 @@ files: group: ipl priority: 2 size: 131072 - required: true note: > X68030 IPL ROM. Alternative to iplrom.dat for X68030 model emulation. Only used if iplrom.dat is not found. @@ -74,7 +78,6 @@ files: group: ipl priority: 3 size: 131072 - required: true note: > X68000 Compact IPL ROM. Alternative to iplrom.dat. Only used if iplrom.dat and iplrom30.dat are not found. @@ -85,7 +88,6 @@ files: group: ipl priority: 4 size: 131072 - required: true note: > X68000 XVI IPL ROM. Last fallback in the IPL search order. Only used if the three other IPL variants are not found. @@ -99,7 +101,6 @@ files: size: 786432 group: font priority: 1 - required: true note: > X68000 character generator ROM. 768 KB, provides the font glyphs for text display. If missing, the core falls back to cgrom.tmp (user-generated font @@ -111,7 +112,6 @@ files: size: 786432 group: font priority: 2 - required: true unsourceable: "generated by WinX68k from the host fonts, no canonical file" note: > Alternative font file, used as fallback when cgrom.dat is absent. diff --git a/emulators/swf-player.yml b/emulators/swf-player.yml index f71f6d1d2..4307825e5 100644 --- a/emulators/swf-player.yml +++ b/emulators/swf-player.yml @@ -104,7 +104,7 @@ files: system: flash required: true bundled: true - variant_group: "ruffle-core" + selection_slot: "ruffle-core" size: 90386 md5: 5b1d64812875a1341ebd2a611e9324a1 sha1: 0eabbed786f3b8e5656d123e9f4573f918ba63d8 @@ -119,7 +119,7 @@ files: system: flash required: true bundled: true - variant_group: "ruffle-core" + selection_slot: "ruffle-core" size: 13107470 md5: 834f85979115fd740943a782887d0ab0 sha1: d27da38e4d37727a44bf04d7690a09135fad7115 @@ -134,7 +134,7 @@ files: system: flash required: true bundled: true - variant_group: "ruffle-core" + selection_slot: "ruffle-core" size: 94169 md5: 5aee868a91c066cc15f8b06999af690b sha1: 45815723a1bdf4229e289906bf4e52dbfd78da56 @@ -149,7 +149,7 @@ files: system: flash required: true bundled: true - variant_group: "ruffle-core" + selection_slot: "ruffle-core" size: 13323601 md5: fe843bd0152c714f0449493b0193108f sha1: fea8e60e86cba7a63258626b13e6adb69ed110f9 diff --git a/emulators/yabadabadoo.yml b/emulators/yabadabadoo.yml index 32f1625a8..e71912d9b 100644 --- a/emulators/yabadabadoo.yml +++ b/emulators/yabadabadoo.yml @@ -58,11 +58,14 @@ notes: | or MPEG ROM path is passed, so no other firmware is read. ref: yabause/src/libretro/libretro.c:1078-1087, yabause/src/libretro/libretro.c:1898-1902 +groups: + saturn-bios: + required: true + files: - name: "saturn_bios.bin" system: sega-saturn group: saturn-bios - required: true max_size: 524288 validation: [max_size] note: "Any region. Second choice for every disc region." @@ -71,7 +74,6 @@ files: - name: "sega_101.bin" system: sega-saturn group: saturn-bios - required: true region: [japan, asia-ntsc, south-korea] max_size: 524288 validation: [max_size] @@ -81,7 +83,6 @@ files: - name: "mpr-17933.bin" system: sega-saturn group: saturn-bios - required: true region: [north-america, europe, oceania, asia-pal, brazil, latin-america] max_size: 524288 validation: [max_size] diff --git a/emulators/yabasanshiro.yml b/emulators/yabasanshiro.yml index 7c281eb80..3afe0efdc 100644 --- a/emulators/yabasanshiro.yml +++ b/emulators/yabasanshiro.yml @@ -80,13 +80,16 @@ notes: | Both build the pi4-update tip and patch the home directory to /storage/.config/yabasanshiro. +groups: + saturn-bios: + required: true + files: - name: "saturn_bios.bin" system: sega-saturn mode: libretro group: saturn-bios priority: 1 - required: true note: "Primary Saturn BIOS. Any region accepted." source_ref: "yabause/src/libretro/libretro.c:1048" @@ -95,7 +98,6 @@ files: mode: libretro group: saturn-bios priority: 2 - required: true note: "Saturn BIOS v1.01 (Japan). Fallback if saturn_bios.bin missing." source_ref: "yabause/src/libretro/libretro.c:1052" @@ -104,6 +106,5 @@ files: mode: libretro group: saturn-bios priority: 3 - required: true note: "Saturn BIOS v1.003 (NA/EU). Last fallback." source_ref: "yabause/src/libretro/libretro.c:1056" diff --git a/emulators/yabause.yml b/emulators/yabause.yml index e086cab20..9344dcc24 100644 --- a/emulators/yabause.yml +++ b/emulators/yabause.yml @@ -47,12 +47,15 @@ notes: | BIOS loading: retro_load_game() at yabause/src/libretro/libretro.c:1035-1049. Core load: LoadBios() at yabause/src/memory.c:1260-1263, T123Load() at yabause/src/memory.h:196-265. +groups: + saturn-bios: + required: true + files: - name: "saturn_bios.bin" system: sega-saturn group: saturn-bios priority: 1 - required: true hle_fallback: true max_size: 524288 validation: [max_size] @@ -63,7 +66,6 @@ files: system: sega-saturn group: saturn-bios priority: 2 - required: true hle_fallback: true max_size: 524288 validation: [max_size] @@ -74,7 +76,6 @@ files: system: sega-saturn group: saturn-bios priority: 3 - required: true hle_fallback: true max_size: 524288 validation: [max_size] diff --git a/emulators/ymir.yml b/emulators/ymir.yml index 507235e2e..f1a2dad6a 100644 --- a/emulators/ymir.yml +++ b/emulators/ymir.yml @@ -78,6 +78,10 @@ notes: | XXH128 hashes used for ROM identification only, not validation. All file loading validates size only. +groups: + ipl: + required: true + files: - name: "sega_101.bin" system: sega-saturn @@ -98,7 +102,6 @@ files: region: [japan] group: ipl priority: 1 - required: true mode: libretro size: 524288 validation: [size] @@ -113,7 +116,6 @@ files: region: [north-america, europe, asia-ntsc, asia-pal, brazil, latin-america] group: ipl priority: 2 - required: true mode: libretro size: 524288 validation: [size] @@ -127,7 +129,6 @@ files: system: sega-saturn group: ipl priority: 3 - required: true mode: libretro size: 524288 validation: [size] diff --git a/emulators/zesarux.yml b/emulators/zesarux.yml index 577731803..cc64d798c 100644 --- a/emulators/zesarux.yml +++ b/emulators/zesarux.yml @@ -1158,7 +1158,6 @@ files: required: false bundled: true system: jupiter-ace - variant_group: "jupiter-ace-rom" source_ref: "src/ALTERNATEROMS:11-12, src/start.c:4877-4881" note: "Jupiter Ace ROM patched by Dutra Lacerda, distinct from the root ace.rom." diff --git a/schemas/emulator.schema.json b/schemas/emulator.schema.json index b6ac0b212..4165c814c 100644 --- a/schemas/emulator.schema.json +++ b/schemas/emulator.schema.json @@ -497,6 +497,19 @@ "type": "string" } }, + "groups": { + "description": "The sets of entries the code takes one of, by name, with the requirement of each. A member names its sets with group and writes no required of its own: it is required when one of its sets is.", + "type": "object", + "minProperties": 1, + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "required": ["required"], + "properties": { + "required": {"type": "boolean"} + } + } + }, "files": { "type": "array", "items": { @@ -679,10 +692,11 @@ "minimum": 0 }, "group": { - "description": "Entries of one profile sharing a group are one need: the code loads one of them (the first it finds, in priority order where the source states one) and holding any member fills it. required states the set's requirement and is the same on every member. A list puts a file in several sets, a fallback two machine models share.", + "description": "The sets of the profile's groups map this entry belongs to. A set is one need: the code loads one member (the first it finds, in priority order where the source states one) and holding any member fills it. A list puts a file in several sets, a fallback two machine models share, and a map from set to rank gives its place in each when the places differ (priority otherwise). The entry writes no required: it is derived from its sets.", "oneOf": [ {"type": "string", "minLength": 1}, - {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "string", "minLength": 1}} + {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "string", "minLength": 1}}, + {"type": "object", "minProperties": 1, "additionalProperties": {"type": ["integer", "null"], "minimum": 0}} ] }, "region_check": { @@ -701,7 +715,8 @@ "directory" ] }, - "variant_group": { + "selection_slot": { + "description": "Files one core chooses between by a rule other than first found: the disc's territory, a core option, a feature probe. They fill one slot of the core whatever system each is filed under, region filtering compares them together, and same-named entries in different slots stay apart. A member does not stand in for another: entries that do are a group.", "type": "string", "minLength": 1 }, @@ -941,6 +956,9 @@ ] } }, + { + "not": {"required": ["group", "required"]} + }, { "if": { "properties": { diff --git a/scripts/alternatives.py b/scripts/alternatives.py index ef15290f5..3893981ff 100644 --- a/scripts/alternatives.py +++ b/scripts/alternatives.py @@ -3,32 +3,88 @@ A profile writes ``group:`` on the entries of one search list. Yabause tries saturn_bios.bin, sega_101.bin, then mpr-17933.bin and loads the first it finds (libretro.c:1035-1049): it needs one of them and none in particular. -``required`` on a member states the set's requirement, and ``priority`` keeps -the order the code reads them in. +The requirement belongs to the set, in the profile's ``groups:`` map, and a +member's ``required`` is derived from the sets it is in: true when one of +them is required. A fallback two machine models share (pom2's apple2.rom) +then needs no value of its own. ``priority`` keeps the order the code reads +the members in. Holding one member fills the set, so the other members are not missing. A -set nobody holds is one gap, reported under its first member, never one gap -per member. Every reader settles sets through ``settle_sets`` so that verify, -the gap report and the packs agree on what is missing. +set nobody holds is one gap, reported under its first member with the set's +requirement, never one gap per member. A --required-only build keeps one +member of a required set, the first the code would load among those held. +Every reader goes through this module so that verify, the gap report, the +packs and the manifests agree. """ from __future__ import annotations -from collections.abc import Hashable, Iterable, Sequence +from collections.abc import Callable, Hashable, Iterable, Sequence +from dataclasses import dataclass _UNRANKED = 1 << 30 def groups_of(entry: dict) -> tuple[str, ...]: - """The sets an entry belongs to: one name, or a list for a file two - machine models both fall back on.""" + """The sets an entry belongs to: one name, a list for a file two machine + models both fall back on, or a map from set to the entry's rank there.""" value = entry.get("group") if not value: return () - values = value if isinstance(value, list) else [value] + values = value if isinstance(value, (list, dict)) else [value] return tuple(str(v) for v in values if v) +def rank_in(entry: dict, group: str) -> int | None: + """The rank of an entry in one of its sets: the map's value when the + entry ranks differently in each (pom2's apple2.rom is second for the ][ + and third at boot), else its priority.""" + value = entry.get("group") + if isinstance(value, dict) and isinstance(value.get(group), int): + return value[group] + priority = entry.get("priority") + return priority if isinstance(priority, int) else None + + +def set_requirements(profile: dict) -> dict[str, bool]: + """The requirement of each set the profile declares.""" + declared = profile.get("groups") or {} + return { + str(name): bool((spec or {}).get("required", True)) + for name, spec in declared.items() + } + + +def derive_member_requirements(profile: dict) -> None: + """Write on each member the requirement its sets give it. + + Read once when a profile is loaded, so every reader sees ``required`` + as it always has. A member is required when one of its sets is. + """ + requirements = set_requirements(profile) + for entry in profile.get("files") or []: + if not isinstance(entry, dict): + continue + names = groups_of(entry) + if names and "required" not in entry: + entry["required"] = any(requirements.get(n, True) for n in names) + + +def required_groups(entry: dict, profile: dict) -> list[str]: + """The required sets of its profile an entry is a member of.""" + requirements = set_requirements(profile) + return [n for n in groups_of(entry) if requirements.get(n, True)] + + +def set_fields(entry: dict, profile: dict) -> dict: + """What a --required-only build reads on a core file to keep one member + of a set: the entry's required sets, each with its rank there.""" + sets = required_groups(entry, profile) + if not sets: + return {} + return {"required_groups": {group: rank_in(entry, group) for group in sets}} + + def set_keys(owner: Hashable, entry: dict) -> tuple[tuple, ...]: """The keys of an entry's sets, scoped to its owner (a profile, or a profile under one build).""" @@ -45,39 +101,48 @@ def slot_set_keys(owner: Hashable, entries: Iterable[dict]) -> tuple[tuple, ...] return tuple(keys) -def _rank(entry: dict, position: int) -> tuple[int, int]: - priority = entry.get("priority") - return (priority if isinstance(priority, int) else _UNRANKED, position) +def _rank(rank: int | None, position: int) -> tuple[int, int]: + return (rank if isinstance(rank, int) else _UNRANKED, position) + + +@dataclass +class Standing: + """The member kept as the one gap of the sets nobody holds.""" + + others: list[str] + required: bool def settle_sets( members: Sequence[tuple[tuple[tuple, ...], dict, bool]], -) -> tuple[set[int], dict[int, list[str]]]: + set_required: Callable[[tuple], bool] = lambda _key: True, +) -> tuple[set[int], dict[int, Standing]]: """Which unheld members are excused, and which one stands for its set. ``members`` lists ``(set keys, entry, fills)`` in the order the profile declares them, where ``fills`` says the member is held (or answered by a declaration that reports it). Returns the positions to leave out of a report and, for each set no member fills, the position kept as its one - gap with the names of the others. A member fills or not on its own: a - held one is never excused, and an unheld one stays reported when it - stands for at least one unfilled set. + gap with the names of the others and the set's requirement. A member + fills or not on its own: a held one is never excused, and an unheld one + stays reported when it stands for at least one unfilled set. """ by_set: dict[tuple, list[int]] = {} for position, (keys, _entry, _fills) in enumerate(members): for key in keys: by_set.setdefault(key, []).append(position) - standing: dict[int, list[str]] = {} - for positions in by_set.values(): + standing: dict[int, Standing] = {} + for key, positions in by_set.items(): if any(members[p][2] for p in positions): continue - first = min(positions, key=lambda p: _rank(members[p][1], p)) - names = standing.setdefault(first, []) + first = min(positions, key=lambda p: _rank(rank_in(members[p][1], key[1]), p)) + held_up = standing.setdefault(first, Standing([], False)) + held_up.required = held_up.required or set_required(key) for p in positions: name = str(members[p][1].get("name", "")) - if p != first and name not in names: - names.append(name) + if p != first and name not in held_up.others: + held_up.others.append(name) excused = { position @@ -85,3 +150,58 @@ def settle_sets( if keys and not fills and position not in standing } return excused, standing + + +def required_only_spares( + entries: Sequence[dict], + owner: Callable[[dict], Hashable], + held: Callable[[dict], bool], + gone: Callable[[dict], bool] = lambda _entry: False, + sets: Callable[[dict], dict[str, int | None]] = lambda e: e.get("required_groups") or {}, +) -> set[int]: + """The ids of the held members a --required-only build leaves out. + + ``sets`` maps the required sets of an entry to its rank in each: a core + file of a platform pack carries them as ``required_groups``. + + A required set needs one file: the build keeps the member the code + would load first among those it holds and still carries (``gone`` says + a region filter took it), by ``priority`` then declaration order, and + drops the others. A member of an optional set is not touched here, + nor one that another required set of its own still needs. + """ + candidates: dict[tuple, list[tuple[tuple[int, int], int]]] = {} + for position, entry in enumerate(entries): + if not held(entry) or gone(entry): + continue + for group, rank in sets(entry).items(): + candidates.setdefault((owner(entry), group), []).append( + (_rank(rank, position), position) + ) + kept: set[int] = set() + members: set[int] = set() + for ranked in candidates.values(): + members.update(position for _rank_key, position in ranked) + kept.add(min(ranked)[1]) + return {id(entries[p]) for p in members - kept} + + +def core_file_spares( + entries: Sequence[dict], gone: Callable[[dict], bool] = lambda _entry: False, +) -> set[int]: + """The core files of a platform pack a --required-only build leaves out. + + A set the platform declares a member of is carried by that declaration, + so its other members are spares. Otherwise one held member per required + set stays, as in an emulator pack. Read by the builder, the manifest + and verify alike. + """ + answered = {id(e) for e in entries if e.get("set_declared")} + rest = [e for e in entries if id(e) not in answered] + return answered | required_only_spares( + rest, + owner=lambda e: e.get("source_profile") or e.get("profile") or "", + held=lambda e: e.get("in_repo", True) is not False, + gone=gone, + ) + diff --git a/scripts/common.py b/scripts/common.py index 2cef0d5d9..0e885d0e2 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -22,6 +22,8 @@ import zipfile from pathlib import Path from typing import Any, TextIO +from alternatives import derive_member_requirements + # Every build script imports this module. verify.py, slots.py, validate_pr.py # and cross_reference.py started under 3.8 and stopped halfway on a # str.removeprefix AttributeError, which named no version. @@ -2346,6 +2348,7 @@ def load_emulator_profiles( if "emulator" not in profile: continue _normalize_hash_fields(profile) + derive_member_requirements(profile) profiles[f.stem] = profile _emulator_profiles_cache[cache_key] = profiles return profiles diff --git a/scripts/cross_reference.py b/scripts/cross_reference.py index 8866b3420..f48d2f5a4 100644 --- a/scripts/cross_reference.py +++ b/scripts/cross_reference.py @@ -23,7 +23,7 @@ from collections import Counter from pathlib import Path sys.path.insert(0, os.path.dirname(__file__)) -from alternatives import set_keys, settle_sets +from alternatives import set_keys, set_requirements, settle_sets from common import ( platform_arg, ProfileSelectionError, @@ -697,7 +697,7 @@ def slot_key(f: dict, standalone: bool = False) -> tuple: f.get("archive"), system_dir_path(f, where or fname), f.get("system"), - f.get("variant_group"), + f.get("selection_slot"), f.get("mode", "both"), read_from_save_dir(f, standalone), ) @@ -886,7 +886,7 @@ def _cross_reference_profile( archive_key = ( archive, f.get("system"), - f.get("variant_group"), + f.get("selection_slot"), f.get("mode", "both"), ) if archive_key not in archive_gaps: @@ -969,12 +969,17 @@ def _cross_reference_profile( # nobody holds is one gap under its first member (alternatives.py), as # verify and the packs count it. grouped = [r for r in set_records if r[0]] - excused, standing = settle_sets([(keys, f, fills) for keys, f, fills, _l, _r in grouped]) + requirements = set_requirements(profile) + excused, standing = settle_sets( + [(keys, f, fills) for keys, f, fills, _l, _r in grouped], + lambda key: requirements.get(key[1], True), + ) for at, (_keys, member, _fills, rows, set_row) in enumerate(grouped): if set_row is None or rows is None: continue if at in standing: - set_row["alternatives"] = standing[at] + set_row["alternatives"] = standing[at].others + set_row["required"] = standing[at].required if at in excused: rows.remove(set_row) fates[position[id(member)]] = "other_versions" diff --git a/scripts/generate_pack.py b/scripts/generate_pack.py index 17adf5979..a6551c33c 100644 --- a/scripts/generate_pack.py +++ b/scripts/generate_pack.py @@ -36,7 +36,7 @@ import zipfile from pathlib import Path, PurePosixPath sys.path.insert(0, os.path.dirname(__file__)) -from alternatives import settle_sets, slot_set_keys +from alternatives import core_file_spares, settle_sets, slot_set_keys from common import resolve_platform_cores as _platform_cores from common import ( installed_data_refs, @@ -1295,8 +1295,13 @@ def generate_pack( db, bios_dir, zip_contents, data_registry, validation_index, validation_owners, offline, profiles=emu_profiles, ) + spares = core_file_spares( + core_files, + lambda fe: bool(region_drops) + and extra_region_key(fe, declared_root(config)) in region_drops, + ) if required_only else set() for fe in core_files: - if required_only and fe.get("required") is False: + if required_only and (fe.get("required") is False or id(fe) in spares): continue dest = sanitize_pack_path(fe.get("destination", fe["name"])) if region_drops and extra_region_key(fe, declared_root(config)) in region_drops: @@ -1434,8 +1439,13 @@ def generate_pack( if system_filter: unfolded = _extras_for_systems(unfolded, system_filter) prefix = _detect_extras_prefix(config, base_dest) + unfolded_spares = core_file_spares( + unfolded, + lambda fe: bool(region_drops) + and extra_region_key(fe, declared_root(config)) in region_drops, + ) if required_only else set() for fe in unfolded: - if required_only and fe.get("required") is False: + if required_only and (fe.get("required") is False or id(fe) in unfolded_spares): continue if region_drops and extra_region_key(fe, declared_root(config)) in region_drops: continue @@ -1826,9 +1836,13 @@ def generate_emulator_pack( # there (raze's seven SW.GRP): the first one the core accepts # fills the slot, as verify --emulator credits it, and the slot # has one outcome however many alternatives it lists. + spares = emulator_required_spares( + emu_name, profile, files, standalone, region_drops, db, + validation_index, zip_contents, data_registry, + ) if required_only else set() slots: dict[str, list[dict]] = {} for fe in files: - if required_only and fe.get("required") is False: + if required_only and (fe.get("required") is False or id(fe) in spares): continue if fe.get("archive"): continue @@ -4398,8 +4412,13 @@ def _manifest_core_entries( db, bios_dir, zip_contents, data_registry, validation_index or {}, owners, offline, profiles=profiles, ) + spares = core_file_spares( + core_files, + lambda fe: bool(region_drops) + and extra_region_key(fe, declared_root(config)) in region_drops, + ) if required_only else set() for fe in core_files: - if required_only and fe.get("required") is False: + if required_only and (fe.get("required") is False or id(fe) in spares): continue dest = sanitize_pack_path(fe.get("destination", fe["name"])) if not dest: @@ -5770,6 +5789,7 @@ from packextras import ( # noqa: E402,F401 extras_for_systems as _extras_for_systems, platform_region_groups, emulator_region_drops, + emulator_required_spares, _emulator_region_group, expand_agnostic_entries, undeclared_report, diff --git a/scripts/generate_site.py b/scripts/generate_site.py index fc14074cb..517b1e9e9 100644 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -1115,7 +1115,7 @@ def _platform_export_rows( ), "required": bool(entry.get("required", True)), "region": _json_text(entry.get("region")), - "variant_group": _json_text(entry.get("variant_group")), + "selection_slot": _json_text(entry.get("selection_slot")), "size": entry.get("size"), "sha1": _json_text(entry.get("sha1")), "sha256": _json_text(entry.get("sha256")), @@ -1371,7 +1371,7 @@ def _write_sqlite_export( CREATE TABLE platform_files ( platform_id TEXT NOT NULL, system TEXT NOT NULL, name TEXT NOT NULL, destination TEXT NOT NULL, required INTEGER NOT NULL, - region TEXT, variant_group TEXT, size INTEGER, + region TEXT, selection_slot TEXT, size INTEGER, sha1 TEXT, sha256 TEXT, md5 TEXT, crc32 TEXT, status TEXT, category TEXT, collection_path TEXT, resolved_sha1 TEXT, zipped_file TEXT, hash_match_mandatory INTEGER, mandatory_declared INTEGER, @@ -1454,7 +1454,7 @@ def _write_sqlite_export( connection.executemany( "INSERT INTO platform_files VALUES " "(:platform_id, :system, :name, :destination, :required, :region, " - ":variant_group, :size, :sha1, :sha256, :md5, :crc32, :status, :category, " + ":selection_slot, :size, :sha1, :sha256, :md5, :crc32, :status, :category, " ":collection_path, :resolved_sha1, :zipped_file, :hash_match_mandatory, " ":mandatory_declared, :curated, :shared_group, :mirror_of, :zip_digest)", platform_files, @@ -1698,7 +1698,7 @@ def generate_data_exports( _csv_document( [ "platform_id", "platform", "system", "name", "destination", - "required", "region", "variant_group", "size", "sha1", + "required", "region", "selection_slot", "size", "sha1", "sha256", "md5", "crc32", "status", "category", "collection_path", "resolved_sha1", *ENTRY_EXPORT_FIELDS, ], diff --git a/scripts/launchers.py b/scripts/launchers.py index 1f9ce1d56..410a907a6 100644 --- a/scripts/launchers.py +++ b/scripts/launchers.py @@ -643,7 +643,7 @@ def _launcher_entry( entry = { key: report.get(key) for key in ( - "profile", "emulator", "systems", "system", "region", "variant_group", + "profile", "emulator", "systems", "system", "region", "selection_slot", "required", "hle_fallback", "category", ) } diff --git a/scripts/packextras.py b/scripts/packextras.py index 29ed24225..8985a23c6 100644 --- a/scripts/packextras.py +++ b/scripts/packextras.py @@ -9,6 +9,7 @@ from __future__ import annotations 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 declared_root, expand_platform_declared_names, platform_declarations from common import load_emulator_profiles @@ -388,8 +389,12 @@ def hidden_extras( ) if system_ids: extras = extras_for_systems(extras, system_ids) + spares = core_file_spares( + extras, + lambda fe: extra_region_key(fe, region_prefix) in (region_drops or set()), + ) if required_only else set() for fe in extras: - if required_only and fe.get("required") is False: + if required_only and (fe.get("required") is False or id(fe) in spares): continue dest = sanitize_pack_path(fe.get("destination", fe.get("name", ""))) if not dest or (region_drops and extra_region_key(fe, region_prefix) in region_drops): @@ -640,7 +645,7 @@ def _agnostic_scan_extras( "source_system": f.get("system"), "source_systems": entry_system_ids(f, profile), "region": f.get("region"), - "variant_group": f.get("variant_group"), + "selection_slot": f.get("selection_slot"), "agnostic_scan": True, } extra["claim_order"] = (4, 0, emu_name) @@ -796,7 +801,7 @@ def _companion_extras( "source_system": seed.get("system"), "source_systems": entry_system_ids(seed, profile), "region": seed.get("region"), - "variant_group": seed.get("variant_group"), + "selection_slot": seed.get("selection_slot"), "agnostic_scan": True, } extra["claim_order"] = (4, 0, emu_name) @@ -970,7 +975,7 @@ def _archive_prefix_copies( "source_system": archive_entry.get("system"), "source_systems": entry_system_ids(archive_entry, profile), "region": archive_entry.get("region"), - "variant_group": archive_entry.get("variant_group"), + "selection_slot": archive_entry.get("selection_slot"), } members = archive_members(archive_name, profile.get("files", [])) if members: @@ -1354,7 +1359,8 @@ def _collect_emulator_extras( "source_system": f.get("system"), "source_systems": entry_system_ids(f, profile), "region": f.get("region"), - "variant_group": f.get("variant_group"), + "selection_slot": f.get("selection_slot"), + **set_fields(f, profile), } # The same identity the first pass carries: without it the copy # resolves on its name alone and a same-named file of another @@ -1522,7 +1528,8 @@ def _report_extra(u: dict, name: str, dest: str) -> dict: "source_system": u.get("system"), "source_systems": u.get("systems", []), "region": u.get("region"), - "variant_group": u.get("variant_group"), + "selection_slot": u.get("selection_slot"), + **{k: u[k] for k in ("required_groups", "set_declared") if k in u}, } if u.get("members"): extra["members"] = u["members"] @@ -1795,9 +1802,9 @@ def extra_region_groups( return (spellings or {}).get(_norm_system_id(sys_id), sys_id) emulator = extra.get("source_emulator", "") - variant = extra.get("variant_group") + variant = extra.get("selection_slot") if variant: - # A variant group is one slot of one core, whatever system each + # A selection slot is one slot of one core, whatever system each # member is filed under: PicoDrive files its US Mega CD BIOS under # sega-segacd and the EU/JP ones under sega-megacd, and find_bios # picks one of the twelve. Keyed as emulator mode keys it. @@ -1826,7 +1833,7 @@ def emulator_region_drops( ) -> set[str]: """Destinations an emulator-mode `--region` withdraws. - One group per system (or explicit variant_group) inside each profile, so + One group per system (or explicit selection_slot) inside each profile, so a multi-system core such as O2EM keeps a fallback BIOS for every system instead of letting one matching system empty another. The emulator pack and `verify --emulator` both read this, keyed by pack destination. @@ -1864,6 +1871,38 @@ def emulator_region_drops( return region_mod.resolve_region_drops(region_groups, region_index, regions, held) +def emulator_required_spares( + emu_name: str, + profile: dict, + files: list[dict], + standalone: bool, + region_drops: set[str], + db: dict, + validation_index: dict, + zip_contents: dict | None, + data_registry: dict | None, +) -> set[int]: + """The members of required sets an emulator --required-only pack leaves + out: one held member stays per set, the first the code loads among those + the pack still carries after the region filter. The pack and + verify --emulator both read this.""" + from alternatives import groups_of, rank_in, required_groups + from packpaths import _resolve_destination + + loose = [fe for fe in files if not fe.get("archive") and groups_of(fe)] + if not loose: + return set() + return required_only_spares( + loose, + owner=lambda _fe: emu_name, + held=lambda fe: _emulator_ships( + fe, emu_name, profile, db, validation_index, zip_contents, data_registry, + ), + gone=lambda fe: _resolve_destination(fe, standalone) in region_drops, + sets=lambda fe: {g: rank_in(fe, g) for g in required_groups(fe, profile)}, + ) + + def _emulator_ships( file_entry: dict, emu_name: str, profile: dict, db: dict, validation_index: dict, zip_contents: dict | None, data_registry: dict | None, @@ -1904,7 +1943,7 @@ def _emulator_region_group( Grouped together, geargrafx's LaserActive pac-n1.bin was dropped by --emulator --region us and kept by --platform --region us. """ - variant = file_entry.get("variant_group") + variant = file_entry.get("selection_slot") if variant: return f"{emu_name}:variant:{variant}" system = file_entry.get("system") diff --git a/scripts/slot.py b/scripts/slot.py index 8ab85b58f..f40281e02 100644 --- a/scripts/slot.py +++ b/scripts/slot.py @@ -127,7 +127,7 @@ def resolve_slot_drops( # A slot is a system AND a declared region: the Japanese and American # PlayStation BIOS are not alternatives to each other, and comparing their # ranks across regions would pick one territory's file for another's. - tiers: dict[tuple[str, tuple[str, ...], tuple], list[tuple[str, int | None]]] = {} + bucketed: dict[tuple[str, tuple[str, ...]], list[tuple[str, int | None, frozenset]]] = {} for group_id, members in groups.items(): for destination, name in members: if held is not None and destination not in held: @@ -136,14 +136,20 @@ def resolve_slot_drops( if entry is None: keep.add(destination) continue - # A rank orders the members of one set: px68k-sdl ranks its IPL - # and its font ROM lists each from 1, and the two never compete. tier = tuple(sorted(entry["regions"])) - sets = tuple(sorted(entry.get("sets") or ())) rank = None if entry.get("conflict") else entry["rank"] - tiers.setdefault((group_id, tier, sets), []).append((destination, rank)) + bucketed.setdefault((group_id, tier), []).append( + (destination, rank, frozenset(entry.get("sets") or ())) + ) + # A rank orders the members of one set: px68k-sdl ranks its IPL and its + # font ROM lists each from 1, and the two never compete. Candidates are + # compared when they share a set, and those in no set among themselves. + tiers: dict[tuple[str, tuple[str, ...], int], list[tuple[str, int | None]]] = {} + for (group_id, tier), found in bucketed.items(): + for part, component in enumerate(_by_shared_set(found)): + tiers[(group_id, tier, part)] = component - for (group_id, tier, _sets), candidates in tiers.items(): + for (group_id, tier, _part), candidates in tiers.items(): if len(candidates) < 2: keep.update(dest for dest, _p in candidates) continue @@ -167,3 +173,28 @@ def resolve_slot_drops( drop.update(dest for dest, _p in candidates if dest != winners[0]) return drop - keep, sorted(undecidable) + + +def _by_shared_set( + found: list[tuple[str, int | None, frozenset]], +) -> list[list[tuple[str, int | None]]]: + """The candidates of one tier split where no set links them.""" + parent = list(range(len(found))) + + def root(i: int) -> int: + while parent[i] != i: + parent[i] = parent[parent[i]] + i = parent[i] + return i + + first_of: dict[object, int] = {} + for i, (_dest, _rank, sets) in enumerate(found): + for key in sets or {None}: + if key in first_of: + parent[root(i)] = root(first_of[key]) + else: + first_of[key] = i + parts: dict[int, list[tuple[str, int | None]]] = {} + for i, (dest, rank, _sets) in enumerate(found): + parts.setdefault(root(i), []).append((dest, rank)) + return list(parts.values()) diff --git a/scripts/validate_schemas.py b/scripts/validate_schemas.py index 0ff95263e..42d8590a8 100644 --- a/scripts/validate_schemas.py +++ b/scripts/validate_schemas.py @@ -124,27 +124,33 @@ def _systems_outside_profile(path: Path, document: object) -> list[str]: def _group_requirement_split(path: Path, document: object) -> list[str]: - """A group whose members disagree on required. + """A set named by a member and not declared, declared and never used, + or a member that writes a required of its own. - The members of a group are one need, so required states the set's and - reads the same on each. A true on the first name only wrote the search - order into required, and the other names read as optional files. + The requirement belongs to the set, in the profile's groups map, and a + member's is derived from it. A value on the member is a second source + that can contradict the first: a true on the first name only wrote the + search order into required, and the other names read as optional. """ from alternatives import groups_of if not isinstance(document, dict): return [] - seen: dict[str, set[bool]] = {} - for entry in document.get("files") or []: + label = path.relative_to(ROOT) + declared = set((document.get("groups") or {}).keys()) + used: set[str] = set() + out: list[str] = [] + for index, entry in enumerate(document.get("files") or []): if not isinstance(entry, dict): continue - for group in groups_of(entry): - seen.setdefault(group, set()).add(bool(entry.get("required", True))) - return [ - f"{path.relative_to(ROOT)}: group {group} mixes required true and false" - for group, values in sorted(seen.items()) - if len(values) > 1 - ] + names = groups_of(entry) + used.update(names) + if names and "required" in entry: + out.append(f"{label}: files[{index}] ({entry.get('name')}) is in a group " + "and writes required: the set's requirement is in groups") + out.extend(f"{label}: group {g} has no entry in groups" for g in sorted(used - declared)) + out.extend(f"{label}: groups declares {g}, which no entry names" for g in sorted(declared - used)) + return out def _checks_without_values(path: Path, document: object) -> list[str]: diff --git a/scripts/verify.py b/scripts/verify.py index 29c64e108..833d2a179 100644 --- a/scripts/verify.py +++ b/scripts/verify.py @@ -37,7 +37,14 @@ import slots sys.path.insert(0, os.path.dirname(__file__)) from hashing import parse_md5_list -from alternatives import set_keys, settle_sets, slot_set_keys +from alternatives import ( + core_file_spares, + set_fields, + set_keys, + set_requirements, + settle_sets, + slot_set_keys, +) from common import ( _foreign_tree, _owner_tokens, @@ -591,7 +598,7 @@ def find_undeclared_files( f.get("archive"), dest, f.get("system"), - f.get("variant_group"), + f.get("selection_slot"), region_key, # Below the save directory each core is handed a folder # of its own: two cores reading hiscore.dat there read @@ -625,7 +632,7 @@ def find_undeclared_files( archive, f.get("system_dir"), f.get("system"), - f.get("variant_group"), + f.get("selection_slot"), region_key, is_standalone, ) @@ -640,7 +647,7 @@ def find_undeclared_files( "systems": entry_system_ids(f, profile), "system": f.get("system"), "region": f.get("region"), - "variant_group": f.get("variant_group"), + "selection_slot": f.get("selection_slot"), "name": archive, "archive": archive, "path": system_dir_path(f, archive), @@ -738,7 +745,7 @@ def find_undeclared_files( "systems": entry_system_ids(f, profile), "system": f.get("system"), "region": f.get("region"), - "variant_group": f.get("variant_group"), + "selection_slot": f.get("selection_slot"), "name": fname, "path": dest, "required": f.get("required", False), @@ -748,6 +755,7 @@ def find_undeclared_files( "in_repo": in_repo, "note": f.get("note", ""), "checks": sorted(checks) if checks else [], + **set_fields(f, profile), "source_ref": f.get("source_ref"), "expected": _build_expected(f, checks), **_identity_of(f), @@ -785,7 +793,7 @@ def find_undeclared_files( undeclared = [u for u in undeclared if id(u) not in settled] undeclared.extend(added) - undeclared = _settle_core_sets(undeclared, set_records, settled) + undeclared = _settle_core_sets(undeclared, set_records, settled, profiles) for entry in sorted(archive_entries.values(), key=lambda e: e["name"]): undeclared.append(entry) @@ -797,6 +805,7 @@ def _settle_core_sets( undeclared: list[dict], records: list[tuple[tuple, dict, bool, tuple[dict, dict | None] | None]], launcher_settled: set[int], + profiles: dict[str, dict], ) -> list[dict]: """The core complement with each set of alternatives settled. @@ -811,14 +820,23 @@ def _settle_core_sets( excused, standing = settle_sets([ (keys, f, fills or (ref is not None and id(ref[0]) in launcher_settled)) for keys, f, fills, ref in grouped - ]) + ], lambda key: set_requirements(profiles.get(key[0][0], {})).get(key[1], True)) + declared_sets = { + key for keys, _f, _fills, ref in grouped if ref is None for key in keys + } dropped: set[int] = set() - for position, (_keys, _f, _fills, ref) in enumerate(grouped): + for position, (keys, _f, _fills, ref) in enumerate(grouped): if ref is None: continue entry, host = ref + required_keys = [k for k in keys if k[1] in entry.get("required_groups", ())] + if required_keys and all(k in declared_sets for k in required_keys): + # The platform declares a member of each of its required sets: + # under --required-only that declaration carries them. + entry["set_declared"] = True if position in standing: - entry["alternatives"] = standing[position] + entry["alternatives"] = standing[position].others + entry["required"] = standing[position].required if position not in excused: continue if host is not None: @@ -1548,6 +1566,10 @@ def verify_platform( ) not in region_drops ] + if required_only: + # One member of a required set, as the builder keeps it. + spares = core_file_spares(undeclared) + undeclared = [u for u in undeclared if id(u) not in spares] _mark_unplaced(config, undeclared, profiles) from packextras import ExtraResolver @@ -2343,6 +2365,14 @@ def verify_emulator( if fe.get("archive") or _resolve_destination(fe, standalone) not in region_drops ] + if required_only: + from packextras import emulator_required_spares + + spares = emulator_required_spares( + emu_name, profile, files, standalone, region_drops, db, + validation_index, zip_contents, data_registry, + ) + files = [fe for fe in files if id(fe) not in spares] # Check data directories (only notice if not cached) for dd in deliverable( @@ -2645,13 +2675,14 @@ def verify_emulator( and result.get("reason") == fallback_reason), ) for _dest, result, _required, _hle, entries in placed_results - ]) + ], lambda key: set_requirements(profile).get(key[1], True)) for position, (dest, result, required, hle, _fe) in enumerate(placed_results): if position in excused and result["status"] == Status.MISSING: set_members.append(result) continue if position in standing: - result["alternatives"] = standing[position] + result["alternatives"] = standing[position].others + result["required"] = required = standing[position].required details.append(result) # Aggregate by destination (path if available, else name) diff --git a/tests/test_e2e.py b/tests/test_e2e.py index 6d727698e..9907b92a9 100644 --- a/tests/test_e2e.py +++ b/tests/test_e2e.py @@ -6790,7 +6790,7 @@ class OneOfASearchList(unittest.TestCase): self.addCleanup(os.chdir, previous) self.db = {"files": {}, "indexes": {"by_md5": {}, "by_name": {}, "by_crc32": {}, "by_path_suffix": {}}} - for name in ("second.bin", "plain.bin"): + for name in ("second.bin", "third.bin", "plain.bin"): data = name.encode() * 8 rel = f"bios/Test/Console/{name}" Path(rel).parent.mkdir(parents=True, exist_ok=True) @@ -6804,11 +6804,12 @@ class OneOfASearchList(unittest.TestCase): def member(name: str, group: str, rank: int) -> dict: return {"name": name, "system": "console-a", "group": group, - "priority": rank, "required": True} + "priority": rank} - self.profile = { + raw = { "emulator": "Seq", "type": "libretro", "cores": ["seq"], "systems": ["console-a"], + "groups": {"first": {"required": True}, "none": {"required": True}}, "files": [ member("first.bin", "first", 1), member("second.bin", "first", 2), @@ -6818,7 +6819,10 @@ class OneOfASearchList(unittest.TestCase): {"name": "plain.bin", "system": "console-a", "required": True}, ], } - Path("emulators/seq.yml").write_text(yaml.safe_dump(self.profile)) + Path("emulators/seq.yml").write_text(yaml.safe_dump(raw)) + self.raw = raw + # What every reader sees: the profile as load_emulator_profiles gives it. + self.profile = load_emulator_profiles("emulators")["seq"] self.config = { "platform": "Seqs", "verification_mode": "existence", "base_destination": "system", "cores": ["seq"], @@ -6832,11 +6836,11 @@ class OneOfASearchList(unittest.TestCase): result = self._verify() reported = {d["name"]: d["status"] for d in result["details"]} self.assertEqual(reported["second.bin"], "ok") + self.assertEqual(reported["third.bin"], "ok") self.assertNotIn("first.bin", reported) - self.assertNotIn("third.bin", reported) self.assertEqual( sorted(d["name"] for d in result["set_members"]), - ["first.bin", "none_b.bin", "third.bin"], + ["first.bin", "none_b.bin"], ) def test_a_set_none_holds_is_one_gap(self): @@ -6848,7 +6852,7 @@ class OneOfASearchList(unittest.TestCase): self.assertEqual(result["severity_counts"][Severity.WARNING], 1) self.assertEqual(result["severity_counts"][Severity.INFO], 0) self.assertEqual(result["status_counts"].get("missing"), 1) - self.assertEqual(result["total_files"], 3) + self.assertEqual(result["total_files"], 4) def test_a_required_set_held_through_a_later_member_is_satisfied(self): """second.bin used to read required: false beside a required @@ -6876,21 +6880,23 @@ class OneOfASearchList(unittest.TestCase): ) with zipfile.ZipFile(zip_path) as zf: packed = sorted(n for n in zf.namelist() if n != "README.txt") - self.assertEqual(packed, ["plain.bin", "second.bin"]) + self.assertEqual(packed, ["plain.bin", "second.bin", "third.bin"]) self.assertEqual(result["severity_counts"]["ok"], len(packed)) lines = out.getvalue().splitlines() self.assertEqual( [line.split(": ", 1)[1] for line in lines if line.strip().startswith("MISSING:")], ["none_a.bin"], ) - self.assertIn("2 files packed, 1 missing", out.getvalue()) + self.assertIn("3 files packed, 1 missing", out.getvalue()) def test_core_complement_reports_one_gap_per_set(self): undeclared = find_undeclared_files( self.config, "emulators", self.db, {"seq": self.profile} ) by_name = {u["name"]: u for u in undeclared} - self.assertEqual(sorted(by_name), ["none_a.bin", "plain.bin", "second.bin"]) + self.assertEqual( + sorted(by_name), ["none_a.bin", "plain.bin", "second.bin", "third.bin"] + ) self.assertTrue(by_name["second.bin"]["in_repo"]) self.assertFalse(by_name["none_a.bin"]["in_repo"]) self.assertEqual(by_name["none_a.bin"]["alternatives"], ["none_b.bin"]) @@ -6905,7 +6911,9 @@ class OneOfASearchList(unittest.TestCase): undeclared = find_undeclared_files( config, "emulators", self.db, {"seq": self.profile} ) - self.assertEqual(sorted(u["name"] for u in undeclared), ["plain.bin", "second.bin"]) + self.assertEqual( + sorted(u["name"] for u in undeclared), ["plain.bin", "second.bin", "third.bin"] + ) def test_gap_report_counts_the_set_once(self): import cross_reference @@ -6919,14 +6927,14 @@ class OneOfASearchList(unittest.TestCase): report: dict = {} cross_reference._cross_reference_profile("seq", self.profile, {}, report, index) gaps = {g["name"]: g for g in report["seq"]["gap_details"]} - self.assertEqual(sorted(gaps), ["none_a.bin", "plain.bin", "second.bin"]) + self.assertEqual(sorted(gaps), ["none_a.bin", "plain.bin", "second.bin", "third.bin"]) self.assertEqual(gaps["none_a.bin"]["alternatives"], ["none_b.bin"]) - self.assertEqual(report["seq"]["set_aside"]["other_versions"], 3) + self.assertEqual(report["seq"]["set_aside"]["other_versions"], 2) def test_truth_names_no_member_required_on_its_own(self): from truth import _fold_into, _new_truth_entry - entry = _new_truth_entry(self.profile["files"][0], "seq", None) + entry = _new_truth_entry(dict(self.profile["files"][0]), "seq", None) self.assertNotIn("required", entry) self.assertEqual(entry["_required_by"], set()) self.assertTrue(entry["one_of_required"]) @@ -6974,10 +6982,56 @@ class OneOfASearchList(unittest.TestCase): from validate_schemas import _group_requirement_split, ROOT path = ROOT / "emulators" / "seq.yml" - split = {"files": [{"name": "a", "group": "g", "required": True}, - {"name": "b", "group": "g", "required": False}]} - self.assertEqual(len(_group_requirement_split(path, split)), 1) - self.assertEqual(_group_requirement_split(path, {"files": self.profile["files"]}), []) + own_value = {"groups": {"g": {"required": True}}, + "files": [{"name": "a", "group": "g", "required": False}, + {"name": "b", "group": "g"}]} + self.assertEqual(len(_group_requirement_split(path, own_value)), 1) + undeclared_set = {"files": [{"name": "a", "group": "g"}]} + self.assertEqual(len(_group_requirement_split(path, undeclared_set)), 1) + unused = {"groups": {"g": {"required": True}}, "files": [{"name": "a"}]} + self.assertEqual(len(_group_requirement_split(path, unused)), 1) + self.assertEqual(_group_requirement_split(path, self.raw), []) + + def test_a_shared_fallback_takes_its_requirement_from_its_sets(self): + """pom2 falls back on apple2e.rom for the //e it boots by default + and for the unenhanced //e it may be switched to. One value on the + file cannot say both: the sets carry it.""" + from alternatives import derive_member_requirements + + profile = {"groups": {"iie": {"required": True}, "iie-u": {"required": False}}, + "files": [{"name": "apple2e_unenh.rom", "group": "iie-u"}, + {"name": "apple2e.rom", "group": ["iie", "iie-u"]}]} + derive_member_requirements(profile) + self.assertEqual([f["required"] for f in profile["files"]], [False, True]) + + def test_required_only_keeps_one_held_member_per_required_set(self): + """second.bin and third.bin are both held: a --required-only pack + carries second.bin, the first the code reads among those held, and + verify reports the same single file.""" + from generate_pack import generate_emulator_pack + + result = verify_emulator(["seq"], "emulators", self.db, + platforms_dir="platforms", required_only=True) + held = sorted(d["name"] for d in result["details"] if d["status"] == "ok") + self.assertEqual(held, ["plain.bin", "second.bin"]) + with contextlib.redirect_stdout(io.StringIO()): + zip_path = generate_emulator_pack( + ["seq"], "emulators", self.db, "bios", "out", offline=True, + platforms_dir="platforms", required_only=True, + ) + with zipfile.ZipFile(zip_path) as zf: + packed = sorted(n for n in zf.namelist() if n != "README.txt") + self.assertEqual(packed, held) + + def test_required_only_core_files_keep_one_member(self): + from alternatives import core_file_spares + + undeclared = find_undeclared_files( + self.config, "emulators", self.db, {"seq": self.profile} + ) + spares = core_file_spares(undeclared) + kept = sorted(u["name"] for u in undeclared if id(u) not in spares) + self.assertEqual(kept, ["none_a.bin", "plain.bin", "second.bin"]) if __name__ == "__main__": unittest.main() diff --git a/tests/test_region_groups.py b/tests/test_region_groups.py index bcc400999..e17edc86f 100644 --- a/tests/test_region_groups.py +++ b/tests/test_region_groups.py @@ -36,9 +36,9 @@ class TheGroupFollowsWhatTheProfileSays(unittest.TestCase): }) self.assertEqual(groups, ["clk:file:MSX/msx-american.rom"]) - def test_a_variant_group_stays_one_slot(self): + def test_a_selection_slot_stays_one_slot(self): self.assertEqual( - extra_region_groups({"source_emulator": "picodrive", "variant_group": "mcd", + extra_region_groups({"source_emulator": "picodrive", "selection_slot": "mcd", "source_systems": ["sega-segacd", "sega-mega-cd"]}), ["picodrive:variant:mcd"], ) @@ -91,10 +91,10 @@ class TheGroupTakesThePlatformSpelling(unittest.TestCase): ["sega-saturn"], ) - def test_file_and_variant_groups_are_untouched(self): + def test_file_and_selection_slots_are_untouched(self): spellings = system_spellings(["sega-megacd"]) self.assertEqual( - extra_region_groups({"source_emulator": "picodrive", "variant_group": "mcd", + extra_region_groups({"source_emulator": "picodrive", "selection_slot": "mcd", "source_system": "sega-megacd"}, spellings), ["picodrive:variant:mcd"], ) diff --git a/wiki/architecture.md b/wiki/architecture.md index f6ac50757..977f0c01a 100644 --- a/wiki/architecture.md +++ b/wiki/architecture.md @@ -275,7 +275,7 @@ own package list, standalone emulators included, and is case-sensitive too, so it gets a separate pack and a separate install manifest. With `--target`, the fingerprint includes target cores so platforms with different hardware filters get separate packs. Emulator and system packs -also retain system identity, `variant_group` and requested region. Same-named +also retain system identity, `selection_slot` and requested region. Same-named regional files are never collapsed merely because their display label matches. ## Pack reproducibility diff --git a/wiki/profiling.md b/wiki/profiling.md index 0fa2b6dc3..4bf3697c8 100644 --- a/wiki/profiling.md +++ b/wiki/profiling.md @@ -603,7 +603,7 @@ look. | Field | Description | |-------|-------------| | `name` | filename as the core expects it | -| `required` | true if the core needs this file to function. On a member of a `group`, the requirement of the set | +| `required` | true if the core needs this file to function. Not written on a member of a `group`: derived from the sets in `groups` | | `system` | system ID this file belongs to (for multi-system profiles) | | `systems` | the systems of the profile this file serves, when it serves several but not all (a MAME device set the machines of two systems load). Never written with `system` | | `archive` | ROM set the file lives inside, for arcade entries (e.g. `neogeo.zip`) | @@ -622,7 +622,8 @@ look. | `region_check` | true if the core refuses to boot a game whose region does not match the BIOS | | `fast_boot` | BIOS generation label the core uses to decide whether fast boot is available | | `priority` | preference order when several BIOS satisfy the same slot, **lowest wins**. DuckStation's `FindBIOSImageInDirectory` keeps the image whose priority is lower, and its table de-prioritizes by raising the number: the buggy launch console sits at 50 and PS2 images at 100, while `scph5501` sits at 5. A core that walks an ordered search list, as PicoDrive does with `biosfiles_us/eu/jp`, maps onto the same field as 1, 2, 3. Only set it from the source. `--one-per-slot` reads it, comparing the members of one `group` | -| `group` | entries the code takes one of: it loads the first it finds (Yabause tries `saturn_bios.bin`, `sega_101.bin`, `mpr-17933.bin`), and holding any member fills the need. Verify, the gap report and the packs report a set nobody holds as one gap under its first member, and no member of a held set as missing. `required` is the same on every member, the order stays in `priority`. A list puts a file in several sets. Where the code keeps a list per region, each list is its own group (PicoDrive's `mega-cd-us`, `mega-cd-eu`, `mega-cd-jp`) | +| `group` | the sets of the profile's `groups:` map an entry belongs to. A set is one need: the code loads one member (the first it finds, as Yabause tries `saturn_bios.bin`, `sega_101.bin`, `mpr-17933.bin`) and holding any member fills it. Verify, the gap report and the packs report a set nobody holds as one gap under its first member, and no member of a held set as missing. A `--required-only` build keeps one member per required set, the first the code reads among those held. A list puts a file in several sets, a map from set to rank gives its place in each when it differs (pom2's `apple2.rom` is second for the ][ and third at boot). Where the code keeps a list per region, each list is its own set (PicoDrive's `mega-cd-us`, `mega-cd-eu`, `mega-cd-jp`) | +| `groups` (profile) | the requirement of each set, `{name: {required: bool}}`. A member writes no `required`: it is required when one of its sets is, so a fallback shared by a required set and an optional one needs no value of its own | | `embedded` | true if the data is compiled into the binary and the external file is optional | | `bundled` | true if the emulator's own release installs the file where it reads it, for the build `mode` names (all builds when `mode` is absent). Packs and the installer leave it out: the user already has it. A file the standalone release ships but the libretro core does not (PPSSPP's assets, Dolphin's Sys) is bundled with `mode: standalone` only | | `not_bundled_on` | platforms (registry keys) whose package leaves out a file the release bundles: their packs bring it (Batocera installs hcl without its data) | @@ -636,7 +637,7 @@ look. | `path` | destination path relative to the system directory, or to the save directory under `load_from: save_dir` | | `standalone_path` | where the standalone build reads the file, under its own data folder, when it differs from `path` (the libretro destination). Packs for a platform running the emulator standalone use it | | `on_mismatch` | what the code does when a declared check fails: `reject` (default) or `warn` and run with it, for every check or check by check (`{size: warn, md5: reject}`) | -| `variant_group` | one slot of one core that several entries fill, whatever system each is filed under (PicoDrive's twelve Mega CD BIOS). Region selection keeps one per group | +| `selection_slot` | files one core chooses between by a rule other than first found (the disc's territory, a core option, a feature probe), one slot of the core whatever system each is filed under: PicoDrive's twelve Mega CD BIOS, picked by disc region, O2EM's four BIOS, picked by a core option. Region selection compares them together and keeps one per slot. A member does not stand in for another: entries that do are a `group` | | `size_note` | how the code treats the file's size when no size field can say it (a read capped at 32 bytes, a file created empty when absent) | | `type` | `directory` when the code reads a whole tree (an RTP, a folder of speech samples). `path` is then the directory, written with its trailing slash, and the entry stands for every file the collection holds under it. A name ending in `/` requires it | | `description` | what this file is | From ee98949d2c14d205c6293bdf28468f28fbdc5944 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Mon, 9 Nov 2026 18:19:47 +0100 Subject: [PATCH 4/6] fix: profile the zip and hog probes at the pin Read while walking the profile checklist on the grouped profiles. - picodrive: find_bios tries each stem as .bin then as .zip (platform/libretro/libretro.c:1316-1326), and pm_open reads the first entry of at least 32 KB or with a ROM extension (pico/cart.c:122-158). The twelve .zip names join their region's set, ranked right after the .bin of the same stem. - dxx-rebirth: the HOG entries cite the mount and the error that stops the game without one (similar/main/inferno.cpp:472, 476, 500-512). --- emulators/dxx-rebirth.yml | 8 +-- emulators/picodrive.yml | 147 +++++++++++++++++++++++++++++++++----- 2 files changed, 132 insertions(+), 23 deletions(-) diff --git a/emulators/dxx-rebirth.yml b/emulators/dxx-rebirth.yml index 917f7760e..5e0ab4998 100644 --- a/emulators/dxx-rebirth.yml +++ b/emulators/dxx-rebirth.yml @@ -82,7 +82,7 @@ files: validation: [size] description: "Descent 1 PC shareware v1.4 HOG" note: "freely redistributable (icculus.org/d2x/data/desc14sw.tar.gz)" - source_ref: "similar/main/mission.cpp:80; common/main/mission.h:67" + source_ref: "similar/main/mission.cpp:80; common/main/mission.h:67; similar/main/inferno.cpp:472, 500-512" - name: descent.pig installed_on: [retropie] @@ -103,7 +103,7 @@ files: validation: [size] description: "Descent 1 PC retail v1.5 HOG (GOG)" note: "commercial, alternative sizes 6856183 (v1.5 alt), 7261423 (v1.0), 7456179 (Mac retail)" - source_ref: "common/main/mission.h:61-64" + source_ref: "common/main/mission.h:61-64; similar/main/inferno.cpp:472, 500-512" - name: descent.pig path: descent.pig @@ -125,7 +125,7 @@ files: validation: [size] description: "Descent 2 PC shareware HOG" note: "freely redistributable (icculus.org/d2x/data/d2shar10.tar.gz)" - source_ref: "similar/main/mission.cpp:107-108; common/main/mission.h:71" + source_ref: "similar/main/mission.cpp:107-108; common/main/mission.h:71; similar/main/inferno.cpp:476, 500-512" - name: d2demo.pig installed_on: [retropie] @@ -156,7 +156,7 @@ files: validation: [size] description: "Descent 2 PC full v1.1-1.2 HOG (GOG)" note: "commercial, alternative sizes 7107354 (v1.0), 7110007 (Mac full), 6132957 (OEM v1.1), 4292746 (Mac shareware)" - source_ref: "common/main/mission.h:73-76" + source_ref: "common/main/mission.h:73-76; similar/main/inferno.cpp:476, 500-512" - name: descent2.ham path: descent2.ham diff --git a/emulators/picodrive.yml b/emulators/picodrive.yml index 30631927b..9d79e521a 100644 --- a/emulators/picodrive.yml +++ b/emulators/picodrive.yml @@ -35,7 +35,8 @@ notes: | and uses the first file found. If no BIOS is found, CD games fail to load with PM_BAD_CD_NO_BIOS. MSU-MD games can run without BIOS. - BIOS filename search order (platform/libretro/libretro.c:1272-1336): + BIOS filename search order (platform/libretro/libretro.c:1272-1336), each + stem tried as .bin then .zip: US: us_scd2_9306, SegaCDBIOS9303, us_scd1_9210, bios_CD_U EU: eu_mcd2_9306, eu_mcd2_9303, eu_mcd1_9210, bios_CD_E JP: jp_mcd2_921222, jp_mcd1_9112, jp_mcd1_9111, bios_CD_J @@ -85,32 +86,59 @@ files: note: "US Sega CD Model 2 BIOS (September 1993). First in US search order. The US list serves every disc whose security byte is neither 0x64 nor 0xa1 (pico/cd/cd_image.c:190, 247-248)." source_ref: "source platform/libretro/libretro.c:1273" - - name: "SegaCDBIOS9303.bin" + - name: "us_scd2_9306.zip" system: sega-megacd selection_slot: "mega-cd-bios" region: [north-america, latin-america] group: mega-cd-us priority: 2 - size: 131072 - note: "US Sega CD BIOS (March 1993). Second in US search order." - source_ref: "source platform/libretro/libretro.c:1273" + note: "A ZIP of the same stem, probed right after us_scd2_9306.bin. pm_open reads its first entry of at least 32 KB or with a ROM extension." + source_ref: "source platform/libretro/libretro.c:1323-1326, source pico/cart.c:122-158" - - name: "us_scd1_9210.bin" + - name: "SegaCDBIOS9303.bin" system: sega-megacd selection_slot: "mega-cd-bios" region: [north-america, latin-america] group: mega-cd-us priority: 3 size: 131072 + note: "US Sega CD BIOS (March 1993). Second in US search order." + source_ref: "source platform/libretro/libretro.c:1273" + + - name: "SegaCDBIOS9303.zip" + system: sega-megacd + selection_slot: "mega-cd-bios" + region: [north-america, latin-america] + group: mega-cd-us + priority: 4 + note: "A ZIP of the same stem, probed right after SegaCDBIOS9303.bin. pm_open reads its first entry of at least 32 KB or with a ROM extension." + source_ref: "source platform/libretro/libretro.c:1323-1326, source pico/cart.c:122-158" + + - name: "us_scd1_9210.bin" + system: sega-megacd + selection_slot: "mega-cd-bios" + region: [north-america, latin-america] + group: mega-cd-us + priority: 5 + size: 131072 note: "US Sega CD Model 1 BIOS (October 1992). Third in US search order." source_ref: "source platform/libretro/libretro.c:1273" + - name: "us_scd1_9210.zip" + system: sega-megacd + selection_slot: "mega-cd-bios" + region: [north-america, latin-america] + group: mega-cd-us + priority: 6 + note: "A ZIP of the same stem, probed right after us_scd1_9210.bin. pm_open reads its first entry of at least 32 KB or with a ROM extension." + source_ref: "source platform/libretro/libretro.c:1323-1326, source pico/cart.c:122-158" + - name: "bios_CD_U.bin" system: sega-megacd selection_slot: "mega-cd-bios" region: [north-america, latin-america] group: mega-cd-us - priority: 4 + priority: 7 size: 131072 note: "US Sega CD BIOS (generic name). Last in US search order." source_ref: "source platform/libretro/libretro.c:1273" @@ -118,6 +146,15 @@ files: # ------------------------------------------------------- # Mega CD / Sega CD - EU region # ------------------------------------------------------- + + - name: "bios_CD_U.zip" + system: sega-megacd + selection_slot: "mega-cd-bios" + region: [north-america, latin-america] + group: mega-cd-us + priority: 8 + note: "A ZIP of the same stem, probed right after bios_CD_U.bin. pm_open reads its first entry of at least 32 KB or with a ROM extension." + source_ref: "source platform/libretro/libretro.c:1323-1326, source pico/cart.c:122-158" - name: "eu_mcd2_9306.bin" system: sega-megacd selection_slot: "mega-cd-bios" @@ -128,32 +165,59 @@ files: note: "EU Mega CD Model 2 BIOS (June 1993). First in EU search order, used when security byte 0x20b is 0x64, the only PAL branch for Australian discs (pico/media.c:243-247, platform/libretro/libretro.c:1306-1308)." source_ref: "source platform/libretro/libretro.c:1276" - - name: "eu_mcd2_9303.bin" + - name: "eu_mcd2_9306.zip" system: sega-megacd selection_slot: "mega-cd-bios" region: [europe, oceania] group: mega-cd-eu priority: 2 - size: 131072 - note: "EU Mega CD Model 2 BIOS (March 1993). Second in EU search order." - source_ref: "source platform/libretro/libretro.c:1276" + note: "A ZIP of the same stem, probed right after eu_mcd2_9306.bin. pm_open reads its first entry of at least 32 KB or with a ROM extension." + source_ref: "source platform/libretro/libretro.c:1323-1326, source pico/cart.c:122-158" - - name: "eu_mcd1_9210.bin" + - name: "eu_mcd2_9303.bin" system: sega-megacd selection_slot: "mega-cd-bios" region: [europe, oceania] group: mega-cd-eu priority: 3 size: 131072 + note: "EU Mega CD Model 2 BIOS (March 1993). Second in EU search order." + source_ref: "source platform/libretro/libretro.c:1276" + + - name: "eu_mcd2_9303.zip" + system: sega-megacd + selection_slot: "mega-cd-bios" + region: [europe, oceania] + group: mega-cd-eu + priority: 4 + note: "A ZIP of the same stem, probed right after eu_mcd2_9303.bin. pm_open reads its first entry of at least 32 KB or with a ROM extension." + source_ref: "source platform/libretro/libretro.c:1323-1326, source pico/cart.c:122-158" + + - name: "eu_mcd1_9210.bin" + system: sega-megacd + selection_slot: "mega-cd-bios" + region: [europe, oceania] + group: mega-cd-eu + priority: 5 + size: 131072 note: "EU Mega CD Model 1 BIOS (October 1992). Third in EU search order." source_ref: "source platform/libretro/libretro.c:1276" + - name: "eu_mcd1_9210.zip" + system: sega-megacd + selection_slot: "mega-cd-bios" + region: [europe, oceania] + group: mega-cd-eu + priority: 6 + note: "A ZIP of the same stem, probed right after eu_mcd1_9210.bin. pm_open reads its first entry of at least 32 KB or with a ROM extension." + source_ref: "source platform/libretro/libretro.c:1323-1326, source pico/cart.c:122-158" + - name: "bios_CD_E.bin" system: sega-megacd selection_slot: "mega-cd-bios" region: [europe, oceania] group: mega-cd-eu - priority: 4 + priority: 7 size: 131072 note: "EU Mega CD BIOS (generic name). Last in EU search order." source_ref: "source platform/libretro/libretro.c:1276" @@ -161,6 +225,15 @@ files: # ------------------------------------------------------- # Mega CD / Sega CD - JP region # ------------------------------------------------------- + + - name: "bios_CD_E.zip" + system: sega-megacd + selection_slot: "mega-cd-bios" + region: [europe, oceania] + group: mega-cd-eu + priority: 8 + note: "A ZIP of the same stem, probed right after bios_CD_E.bin. pm_open reads its first entry of at least 32 KB or with a ROM extension." + source_ref: "source platform/libretro/libretro.c:1323-1326, source pico/cart.c:122-158" - name: "jp_mcd2_921222.bin" system: sega-megacd selection_slot: "mega-cd-bios" @@ -171,32 +244,59 @@ files: note: "JP Mega CD Model 2 BIOS (December 1992). First in JP search order." source_ref: "source platform/libretro/libretro.c:1279" - - name: "jp_mcd1_9112.bin" + - name: "jp_mcd2_921222.zip" system: sega-megacd selection_slot: "mega-cd-bios" region: [japan] group: mega-cd-jp priority: 2 - size: 131072 - note: "JP Mega CD Model 1 BIOS (December 1991). Second in JP search order." - source_ref: "source platform/libretro/libretro.c:1279" + note: "A ZIP of the same stem, probed right after jp_mcd2_921222.bin. pm_open reads its first entry of at least 32 KB or with a ROM extension." + source_ref: "source platform/libretro/libretro.c:1323-1326, source pico/cart.c:122-158" - - name: "jp_mcd1_9111.bin" + - name: "jp_mcd1_9112.bin" system: sega-megacd selection_slot: "mega-cd-bios" region: [japan] group: mega-cd-jp priority: 3 size: 131072 + note: "JP Mega CD Model 1 BIOS (December 1991). Second in JP search order." + source_ref: "source platform/libretro/libretro.c:1279" + + - name: "jp_mcd1_9112.zip" + system: sega-megacd + selection_slot: "mega-cd-bios" + region: [japan] + group: mega-cd-jp + priority: 4 + note: "A ZIP of the same stem, probed right after jp_mcd1_9112.bin. pm_open reads its first entry of at least 32 KB or with a ROM extension." + source_ref: "source platform/libretro/libretro.c:1323-1326, source pico/cart.c:122-158" + + - name: "jp_mcd1_9111.bin" + system: sega-megacd + selection_slot: "mega-cd-bios" + region: [japan] + group: mega-cd-jp + priority: 5 + size: 131072 note: "JP Mega CD Model 1 BIOS (November 1991). Third in JP search order." source_ref: "source platform/libretro/libretro.c:1279" + - name: "jp_mcd1_9111.zip" + system: sega-megacd + selection_slot: "mega-cd-bios" + region: [japan] + group: mega-cd-jp + priority: 6 + note: "A ZIP of the same stem, probed right after jp_mcd1_9111.bin. pm_open reads its first entry of at least 32 KB or with a ROM extension." + source_ref: "source platform/libretro/libretro.c:1323-1326, source pico/cart.c:122-158" + - name: "bios_CD_J.bin" system: sega-megacd selection_slot: "mega-cd-bios" region: [japan] group: mega-cd-jp - priority: 4 + priority: 7 size: 131072 note: "JP Mega CD BIOS (generic name). Last in JP search order." source_ref: "source platform/libretro/libretro.c:1279" @@ -204,6 +304,15 @@ files: # ------------------------------------------------------- # Sega 32X - HLE available, no frontend loads these # ------------------------------------------------------- + + - name: "bios_CD_J.zip" + system: sega-megacd + selection_slot: "mega-cd-bios" + region: [japan] + group: mega-cd-jp + priority: 8 + note: "A ZIP of the same stem, probed right after bios_CD_J.bin. pm_open reads its first entry of at least 32 KB or with a ROM extension." + source_ref: "source platform/libretro/libretro.c:1323-1326, source pico/cart.c:122-158" - name: "32X_G_BIOS.BIN" system: sega-32x required: false From 58735286a7d931bb0bf51148f2d506db8949b1f9 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Mon, 9 Nov 2026 18:26:05 +0100 Subject: [PATCH 5/6] feat: let an archive join a set Kronos reads kronos/stvbios.zip, else stvbios.zip at the root of the system directory, and a ST-V game stops without either (libretro.c:1608-1617, 1676-1681 at the pin). The archive under archive_prefix and the root copy are one need, but only loose entries could be members, so the required-only pack carried both. An archive's own entry now joins a set like a loose file. verify --emulator and the emulator pack settle archive units with the loose entries, the required-only pick covers them, find_undeclared_files and cross_reference record them, and the unit under archive_prefix ranks before the same name at the root. Its members still settle the archive by their contents. Kronos' stvbios.zip is grouped stv-bios. No other profile reads one archive under two paths: odin and tgpulse_next_m1 place their own entries at the prefix path, and flycast's naomi.zip has one path per build. --- emulators/kronos.yml | 6 ++- scripts/alternatives.py | 7 +++- scripts/cross_reference.py | 21 ++++++++-- scripts/generate_pack.py | 35 ++++++++++------ scripts/packextras.py | 47 ++++++++++++++------- scripts/verify.py | 52 +++++++++++++---------- tests/test_e2e.py | 86 ++++++++++++++++++++++++++++++++++++++ wiki/profiling.md | 2 +- 8 files changed, 201 insertions(+), 55 deletions(-) diff --git a/emulators/kronos.yml b/emulators/kronos.yml index 8090a1923..16ed978ee 100644 --- a/emulators/kronos.yml +++ b/emulators/kronos.yml @@ -51,6 +51,8 @@ archive_prefix: kronos groups: saturn-bios: required: true + stv-bios: + required: true files: # ----------------------------------------------------------- @@ -104,8 +106,8 @@ files: # ----------------------------------------------------------- - name: "stvbios.zip" system: sega-stv - required: true - note: "MAME-style ZIP containing ST-V region BIOS EPROMs. Searched in kronos/ subdir first." + group: stv-bios + note: "MAME-style ZIP containing ST-V region BIOS EPROMs. Read as kronos/stvbios.zip, else at the root of the system directory, and a ST-V game stops without either (libretro.c:1676-1681)." source_ref: "source yabause/src/libretro/libretro.c:1608-1617, source yabause/src/ctrl/src/stv.c:112-136" # ----------------------------------------------------------- diff --git a/scripts/alternatives.py b/scripts/alternatives.py index 3893981ff..e083e4a79 100644 --- a/scripts/alternatives.py +++ b/scripts/alternatives.py @@ -197,7 +197,12 @@ def core_file_spares( and verify alike. """ answered = {id(e) for e in entries if e.get("set_declared")} - rest = [e for e in entries if id(e) not in answered] + # An archive under the core's own folder is read before the same name + # at the root (Kronos), so archive units rank first among equals. + rest = sorted( + (e for e in entries if id(e) not in answered), + key=lambda e: not e.get("archive"), + ) return answered | required_only_spares( rest, owner=lambda e: e.get("source_profile") or e.get("profile") or "", diff --git a/scripts/cross_reference.py b/scripts/cross_reference.py index f48d2f5a4..d5af100b4 100644 --- a/scripts/cross_reference.py +++ b/scripts/cross_reference.py @@ -801,6 +801,7 @@ def _cross_reference_profile( # Grouped entries met, as (set keys, entry, fills, the list holding # its row, the row): settled once the profile is read. set_records: list[tuple[tuple, dict, bool, list | None, dict | None]] = [] + archive_rows: list[dict] = [] def fate(f: dict, what: str) -> None: key = position[id(f)] @@ -908,6 +909,15 @@ def _cross_reference_profile( "archive_required_count": 0, } entry = archive_gaps[archive_key] + if entry["archive_file_count"] == 0: + # The archive's own declaration names its sets: an archive + # is a member like a loose file (Kronos' stvbios.zip). + own = archive_entry(archive, emu_files) + set_records.append(( + set_keys((emu_name, is_standalone), own), own, + entry["in_repo"], archive_rows, entry, + )) + archive_rows.append(entry) entry["archive_file_count"] += 1 if f.get("required", False): entry["archive_required_count"] += 1 @@ -968,7 +978,10 @@ def _cross_reference_profile( # One need per set: a member another one fills is not a gap, and a set # nobody holds is one gap under its first member (alternatives.py), as # verify and the packs count it. - grouped = [r for r in set_records if r[0]] + # Archive units rank first among equals, as the builder orders them. + grouped = sorted( + (r for r in set_records if r[0]), key=lambda r: r[3] is not archive_rows + ) requirements = set_requirements(profile) excused, standing = settle_sets( [(keys, f, fills) for keys, f, fills, _l, _r in grouped], @@ -982,10 +995,12 @@ def _cross_reference_profile( set_row["required"] = standing[at].required if at in excused: rows.remove(set_row) - fates[position[id(member)]] = "other_versions" + if id(member) in position: + fates[position[id(member)]] = "other_versions" for ag in sorted(archive_gaps.values(), key=lambda e: e["name"]): - gaps.append(ag) + if any(ag is row for row in archive_rows): + gaps.append(ag) tally = Counter(fates.values()) report[emu_name] = { diff --git a/scripts/generate_pack.py b/scripts/generate_pack.py index a6551c33c..559889acc 100644 --- a/scripts/generate_pack.py +++ b/scripts/generate_pack.py @@ -1786,6 +1786,16 @@ def generate_emulator_pack( if archive: archives.add(archive) + # What each destination came to, settled by set once the + # profile is read: a member another member fills is not missing, + # and a set nobody holds is reported once (alternatives.py). An + # archive unit is a member like a loose file. + outcomes: list[tuple[list[dict], bool, Callable[[], None] | None]] = [] + spares = emulator_required_spares( + emu_name, profile, files, standalone, region_drops, db, + validation_index, zip_contents, data_registry, + ) if required_only else set() + # Pack archives as units archive_prefix = profile.get("archive_prefix", "") for archive_name in sorted(archives): @@ -1796,6 +1806,8 @@ def generate_emulator_pack( own_entry = archive_entry(archive_name, files) if required_only and archive_is_optional(own_entry, archive_name, files): continue + if archive_dest in spares: + continue local_path, status = resolve_file( own_entry, db, @@ -1814,7 +1826,11 @@ def generate_emulator_pack( if not (taken and local_path and _same_bytes( local_path, packed_at.get(archive_dest.lower()) )): - report_missing(archive_name, archive_dest) + outcomes.append(([own_entry], False, functools.partial( + report_missing, archive_name, archive_dest + ))) + else: + outcomes.append(([own_entry], True, None)) continue if local_path and status not in ("not_found",): if local_path.endswith(".zip"): @@ -1828,35 +1844,30 @@ def generate_emulator_pack( total_files += 1 if record is not None: record.files.append(archive_dest) + outcomes.append(([own_entry], True, None)) else: - report_missing(archive_name, archive_dest) + outcomes.append(([own_entry], False, functools.partial( + report_missing, archive_name, archive_dest + ))) # Pack individual files (skip archive members). The entries of one # profile on one destination are the releases the code accepts # there (raze's seven SW.GRP): the first one the core accepts # fills the slot, as verify --emulator credits it, and the slot # has one outcome however many alternatives it lists. - spares = emulator_required_spares( - emu_name, profile, files, standalone, region_drops, db, - validation_index, zip_contents, data_registry, - ) if required_only else set() slots: dict[str, list[dict]] = {} for fe in files: - if required_only and (fe.get("required") is False or id(fe) in spares): + if required_only and fe.get("required") is False: continue if fe.get("archive"): continue dest = _resolve_destination(fe, standalone) - if not dest: + if not dest or dest in spares: continue if region_drops and dest in region_drops: continue slots.setdefault(dest, []).append(fe) - # What each destination came to, settled by set once the loop - # is over: a member another member fills is not missing, and a - # set nobody holds is reported once (alternatives.py). - outcomes: list[tuple[list[dict], bool, Callable[[], None] | None]] = [] for dest, alternatives in slots.items(): # Name order, first file wins, case folded: verify replays # the rule and reports a later profile's file as missing. diff --git a/scripts/packextras.py b/scripts/packextras.py index 8985a23c6..c4f315d69 100644 --- a/scripts/packextras.py +++ b/scripts/packextras.py @@ -1881,26 +1881,43 @@ def emulator_required_spares( validation_index: dict, zip_contents: dict | None, data_registry: dict | None, -) -> set[int]: - """The members of required sets an emulator --required-only pack leaves - out: one held member stays per set, the first the code loads among those - the pack still carries after the region filter. The pack and - verify --emulator both read this.""" +) -> set[str]: + """The destinations an emulator --required-only pack leaves out: of each + required set, one held member stays, the first the code loads among + those the pack still carries after the region filter. An archive unit + is a member like a loose file: Kronos reads kronos/stvbios.zip, else + stvbios.zip. The pack and verify --emulator both read this.""" from alternatives import groups_of, rank_in, required_groups - from packpaths import _resolve_destination + from common import archive_entry + from packpaths import _archive_destination, _resolve_destination - loose = [fe for fe in files if not fe.get("archive") and groups_of(fe)] - if not loose: + candidates: list[dict] = [] + archives: list[str] = [] + for fe in files: + archive = fe.get("archive") + if archive and archive not in archives: + archives.append(archive) + own = archive_entry(archive, files) + if groups_of(own): + candidates.append({"dest": _archive_destination( + archive, profile.get("archive_prefix", ""), + profile.get("pack_structure"), standalone, + ), "entry": own}) + for fe in files: + if not fe.get("archive") and groups_of(fe): + candidates.append({"dest": _resolve_destination(fe, standalone), "entry": fe}) + if not candidates: return set() - return required_only_spares( - loose, - owner=lambda _fe: emu_name, - held=lambda fe: _emulator_ships( - fe, emu_name, profile, db, validation_index, zip_contents, data_registry, + spares = required_only_spares( + candidates, + owner=lambda _c: emu_name, + held=lambda c: _emulator_ships( + c["entry"], emu_name, profile, db, validation_index, zip_contents, data_registry, ), - gone=lambda fe: _resolve_destination(fe, standalone) in region_drops, - sets=lambda fe: {g: rank_in(fe, g) for g in required_groups(fe, profile)}, + gone=lambda c: c["dest"] in region_drops, + sets=lambda c: {g: rank_in(c["entry"], g) for g in required_groups(c["entry"], profile)}, ) + return {c["dest"] for c in candidates if id(c) in spares} def _emulator_ships( diff --git a/scripts/verify.py b/scripts/verify.py index 833d2a179..f91b83359 100644 --- a/scripts/verify.py +++ b/scripts/verify.py @@ -665,6 +665,14 @@ def find_undeclared_files( } if f.get("system_dir"): archive_entries[archive_key]["system_dir"] = f["system_dir"] + # The archive's own declaration names its sets: an + # archive is a member like a loose file. + own = archive_entry(archive, profile.get("files", [])) + archive_entries[archive_key].update(set_fields(own, profile)) + set_records.append(( + set_keys((emu_name, is_standalone), own), own, in_repo, + (archive_entries[archive_key], None), + )) entry = archive_entries[archive_key] entry["archive_file_count"] += 1 if f.get("required", False): @@ -793,12 +801,10 @@ def find_undeclared_files( undeclared = [u for u in undeclared if id(u) not in settled] undeclared.extend(added) - undeclared = _settle_core_sets(undeclared, set_records, settled, profiles) - for entry in sorted(archive_entries.values(), key=lambda e: e["name"]): undeclared.append(entry) - return undeclared + return _settle_core_sets(undeclared, set_records, settled, profiles) def _settle_core_sets( @@ -814,7 +820,11 @@ def _settle_core_sets( holds keeps one entry, its first member, naming the others. A claim another profile shares stays: its own core may need that very file. """ - grouped = [r for r in records if r[0]] + # Archive units rank first among equals, as the builder orders them. + grouped = sorted( + (r for r in records if r[0]), + key=lambda r: not (r[3] is not None and r[3][0].get("archive")), + ) if not grouped: return undeclared excused, standing = settle_sets([ @@ -2365,6 +2375,7 @@ def verify_emulator( if fe.get("archive") or _resolve_destination(fe, standalone) not in region_drops ] + spares: set[str] = set() if required_only: from packextras import emulator_required_spares @@ -2372,7 +2383,10 @@ def verify_emulator( emu_name, profile, files, standalone, region_drops, db, validation_index, zip_contents, data_registry, ) - files = [fe for fe in files if id(fe) not in spares] + files = [ + fe for fe in files + if fe.get("archive") or _resolve_destination(fe, standalone) not in spares + ] # Check data directories (only notice if not cached) for dd in deliverable( @@ -2403,6 +2417,12 @@ def verify_emulator( occupy_trees(emu_name, profile) continue + # Each destination's outcome, settled by set once the profile is + # read: an archive unit is a member like a loose file (Kronos reads + # kronos/stvbios.zip, else stvbios.zip). + placed_results: list[tuple[str, dict, bool, bool, list[dict]]] = [] + fallback_reason = "declared hash contradicted by the local file" + # Verify archives as units (e.g., neogeo.zip, aes.zip) seen_archives: set[str] = set() for file_entry in files: @@ -2410,6 +2430,11 @@ def verify_emulator( if archive and archive not in seen_archives: seen_archives.add(archive) own_entry = archive_entry(archive, files) + if _archive_destination( + archive, profile.get("archive_prefix", ""), + profile.get("pack_structure"), standalone, + ) in spares: + continue local_path, resolve_status = pending(own_entry, *resolve_file( own_entry, db, @@ -2452,7 +2477,6 @@ def verify_emulator( result["system"] = file_entry.get("system", "") result["hle_fallback"] = False result["ground_truth"] = build_ground_truth(archive, validation_index) - details.append(result) # Where the pack puts it: an archive_prefix or pack_structure # moves it, and a loose same-named entry is another file. dest = _archive_destination( @@ -2476,27 +2500,13 @@ def verify_emulator( elif local_path: occupy(dest, local_path, emu_name) packed_dests.add(dest) - dest_to_name[dest] = archive - cur = result["status"] - prev = file_status.get(dest) - if prev is None or _STATUS_ORDER.get(cur, 0) > _STATUS_ORDER.get( - prev, 0 - ): - file_status[dest] = cur - sev = _emulator_severity(cur, required, False) - prev_sev = file_severity.get(dest) - if prev_sev is None or _SEVERITY_ORDER.get( - sev, 0 - ) > _SEVERITY_ORDER.get(prev_sev, 0): - file_severity[dest] = sev + placed_results.append((dest, result, required, False, [own_entry])) # Results of this emulator, by destination. Several entries of one # profile on one destination are the releases the code accepts # there (a shareware and a retail HOG, each GRP in a size table): # holding one of them fills the slot, and the others are not gaps. emu_results: dict[str, list[tuple[dict, bool, bool, dict]]] = {} - placed_results: list[tuple[str, dict, bool, bool, list[dict]]] = [] - fallback_reason = "declared hash contradicted by the local file" for file_entry in files: # Skip archive members (verified as archive units above) diff --git a/tests/test_e2e.py b/tests/test_e2e.py index 9907b92a9..aa943c7ac 100644 --- a/tests/test_e2e.py +++ b/tests/test_e2e.py @@ -7033,5 +7033,91 @@ class OneOfASearchList(unittest.TestCase): kept = sorted(u["name"] for u in undeclared if id(u) not in spares) self.assertEqual(kept, ["none_a.bin", "plain.bin", "second.bin"]) +class AnArchiveIsAMemberOfASet(unittest.TestCase): + """Kronos reads kronos/stvbios.zip, else stvbios.zip at the root: one + archive under two paths, a first-found pair. The archive unit and the + root copy settle as members of one set, and a --required-only pack + carries the first, while the members inside still settle the archive + by its contents.""" + + def _fixture(self, held: bool) -> dict: + import zlib + + from common import _emulator_profiles_cache + + _emulator_profiles_cache.clear() + repo = Path(__file__).resolve().parent.parent + (repo / "tmp").mkdir(exist_ok=True) + root = Path(tempfile.mkdtemp(dir=repo / "tmp")) + self.addCleanup(shutil.rmtree, root, True) + previous = os.getcwd() + os.chdir(root) + self.addCleanup(os.chdir, previous) + self.db = {"files": {}, "indexes": {"by_md5": {}, "by_name": {}, + "by_crc32": {}, "by_path_suffix": {}}} + rom = b"STV" * 64 + if held: + rel = "bios/Sega/STV/set.zip" + Path(rel).parent.mkdir(parents=True) + with zipfile.ZipFile(rel, "w") as zf: + zf.writestr("epr.ic8", rom) + data = Path(rel).read_bytes() + sha1 = hashlib.sha1(data).hexdigest() + self.db["files"][sha1] = {"path": rel, "name": "set.zip", "size": len(data), + "md5": hashlib.md5(data).hexdigest(), "crc32": ""} + self.db["indexes"]["by_name"]["set.zip"] = [sha1] + Path("emulators").mkdir() + Path("platforms").mkdir() + Path("emulators/stv.yml").write_text(yaml.safe_dump({ + "emulator": "Stv", "type": "libretro", "cores": ["stv"], + "systems": ["sega-stv"], "archive_prefix": "stv", + "groups": {"stv-bios": {"required": True}}, + "files": [ + {"name": "set.zip", "system": "sega-stv", "group": "stv-bios"}, + {"name": "epr.ic8", "archive": "set.zip", "system": "sega-stv", + "required": True, "crc32": f"{zlib.crc32(rom):08x}"}, + ], + })) + return load_emulator_profiles("emulators")["stv"] + + def test_the_archive_and_its_root_copy_are_one_need(self): + self._fixture(held=False) + result = verify_emulator(["stv"], "emulators", self.db, platforms_dir="platforms") + missing = [d for d in result["details"] if d["status"] == "missing"] + self.assertEqual([d["destination"] for d in missing], ["stv/set.zip"]) + self.assertEqual(missing[0]["alternatives"], ["set.zip"]) + self.assertEqual(result["status_counts"].get("missing"), 1) + + def test_required_only_carries_the_archive_the_code_reads_first(self): + from generate_pack import generate_emulator_pack + + self._fixture(held=True) + full = verify_emulator(["stv"], "emulators", self.db, platforms_dir="platforms") + self.assertEqual( + sorted(d["destination"] for d in full["details"] if d["status"] == "ok"), + ["set.zip", "stv/set.zip"], + ) + result = verify_emulator(["stv"], "emulators", self.db, + platforms_dir="platforms", required_only=True) + held = sorted(d["destination"] for d in result["details"] if d["status"] == "ok") + self.assertEqual(held, ["stv/set.zip"]) + with contextlib.redirect_stdout(io.StringIO()): + zip_path = generate_emulator_pack( + ["stv"], "emulators", self.db, "bios", "out", offline=True, + platforms_dir="platforms", required_only=True, + ) + with zipfile.ZipFile(zip_path) as zf: + packed = sorted(n for n in zf.namelist() if n != "README.txt") + self.assertEqual(packed, held) + + def test_core_files_report_the_pair_once(self): + profile = self._fixture(held=False) + config = {"platform": "Stvs", "verification_mode": "existence", + "base_destination": "system", "cores": ["stv"], + "systems": {"sega-stv": {"files": []}}} + undeclared = find_undeclared_files(config, "emulators", self.db, {"stv": profile}) + self.assertEqual([u["name"] for u in undeclared], ["set.zip"]) + self.assertTrue(undeclared[0].get("archive")) + if __name__ == "__main__": unittest.main() diff --git a/wiki/profiling.md b/wiki/profiling.md index 4bf3697c8..69e6ea50e 100644 --- a/wiki/profiling.md +++ b/wiki/profiling.md @@ -622,7 +622,7 @@ look. | `region_check` | true if the core refuses to boot a game whose region does not match the BIOS | | `fast_boot` | BIOS generation label the core uses to decide whether fast boot is available | | `priority` | preference order when several BIOS satisfy the same slot, **lowest wins**. DuckStation's `FindBIOSImageInDirectory` keeps the image whose priority is lower, and its table de-prioritizes by raising the number: the buggy launch console sits at 50 and PS2 images at 100, while `scph5501` sits at 5. A core that walks an ordered search list, as PicoDrive does with `biosfiles_us/eu/jp`, maps onto the same field as 1, 2, 3. Only set it from the source. `--one-per-slot` reads it, comparing the members of one `group` | -| `group` | the sets of the profile's `groups:` map an entry belongs to. A set is one need: the code loads one member (the first it finds, as Yabause tries `saturn_bios.bin`, `sega_101.bin`, `mpr-17933.bin`) and holding any member fills it. Verify, the gap report and the packs report a set nobody holds as one gap under its first member, and no member of a held set as missing. A `--required-only` build keeps one member per required set, the first the code reads among those held. A list puts a file in several sets, a map from set to rank gives its place in each when it differs (pom2's `apple2.rom` is second for the ][ and third at boot). Where the code keeps a list per region, each list is its own set (PicoDrive's `mega-cd-us`, `mega-cd-eu`, `mega-cd-jp`) | +| `group` | the sets of the profile's `groups:` map an entry belongs to. A set is one need: the code loads one member (the first it finds, as Yabause tries `saturn_bios.bin`, `sega_101.bin`, `mpr-17933.bin`) and holding any member fills it. Verify, the gap report and the packs report a set nobody holds as one gap under its first member, and no member of a held set as missing. A `--required-only` build keeps one member per required set, the first the code reads among those held. A list puts a file in several sets, a map from set to rank gives its place in each when it differs (pom2's `apple2.rom` is second for the ][ and third at boot). Where the code keeps a list per region, each list is its own set (PicoDrive's `mega-cd-us`, `mega-cd-eu`, `mega-cd-jp`). An archive's own entry joins a set like a loose file, its unit under `archive_prefix` ranking before the same name at the root (Kronos reads `kronos/stvbios.zip`, else `stvbios.zip`), and its members still settle it by their contents | | `groups` (profile) | the requirement of each set, `{name: {required: bool}}`. A member writes no `required`: it is required when one of its sets is, so a fallback shared by a required set and an optional one needs no value of its own | | `embedded` | true if the data is compiled into the binary and the external file is optional | | `bundled` | true if the emulator's own release installs the file where it reads it, for the build `mode` names (all builds when `mode` is absent). Packs and the installer leave it out: the user already has it. A file the standalone release ships but the libretro core does not (PPSSPP's assets, Dolphin's Sys) is bundled with `mode: standalone` only | From 50f9b642a0f98b1b91a167d67d1f0bf10bddcade Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Mon, 9 Nov 2026 18:32:53 +0100 Subject: [PATCH 6/6] fix: group the boot ROM slot Pizza Boy SC fills Read while walking the profile checklist on the profiles the variant_group conversion touched. - pizza-boy-sc: the core holds one Mega CD slot and one Master System slot, filled through the document picker under any name, and tests only the Mega CD length (libsc.so frontend/android/src/lib.rs:475, 569-648, megacd_main_machine.rs:169-178). Nothing picks by territory, so the three images of each are a group, not a selection slot: megacd-boot-rom required, mastersystem-boot-rom optional. - fpse, fpse-ng: the BIOS note pointed at "the other images of the group", a set the profile does not list. It now says any 524288-byte PlayStation image fills the slot. --- emulators/fpse-ng.yml | 2 +- emulators/fpse.yml | 2 +- emulators/pizza-boy-sc.yml | 24 ++++++++++++------------ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/emulators/fpse-ng.yml b/emulators/fpse-ng.yml index 9cbf99479..9897c8943 100644 --- a/emulators/fpse-ng.yml +++ b/emulators/fpse-ng.yml @@ -149,7 +149,7 @@ files: md5: 924e392ed05558ffdb115408c263dccf sha1: 10155d8d6e6e832d6ea66db9bc098321fb5e8ebf description: "SCPH-1001, DTL-H1201, DTL-H3001 (v2.2 12-04-95 A)" - note: "The only image the code identifies, and it identifies it by a checksum of its own rather than by a hash it spells out: the 524288 bytes summed as signed bytes, accepted at 85958. The md5 and sha1 here are the dump that reproduces that sum. Selecting a BIOS by hand takes any .bin of exactly 524288 bytes, so the other images of the group fill the slot without being recognised by name or by sum." + note: "The only image the code identifies, and it identifies it by a checksum of its own rather than by a hash it spells out: the 524288 bytes summed as signed bytes, accepted at 85958. The md5 and sha1 here are the dump that reproduces that sum. Selecting a BIOS by hand takes any .bin of exactly 524288 bytes, so any other PlayStation image of that size fills the slot without being recognised by name or by sum." source_ref: "Main.java:24466-24487, Main.java:9945-9952, Main.java:30177-30192, Main.java:5392-5396, libfpse.so 0x70e20-0x70ed0" - name: kanji.rom diff --git a/emulators/fpse.yml b/emulators/fpse.yml index d395957e1..6386b8238 100644 --- a/emulators/fpse.yml +++ b/emulators/fpse.yml @@ -121,7 +121,7 @@ files: md5: 924e392ed05558ffdb115408c263dccf sha1: 10155d8d6e6e832d6ea66db9bc098321fb5e8ebf description: "SCPH-1001, DTL-H1201, DTL-H3001 (v2.2 12-04-95 A)" - note: "The only image the code identifies, and it identifies it by a checksum of its own rather than by a hash it spells out: the 524288 bytes summed as signed bytes, accepted at 85958. The md5 and sha1 here are the dump that reproduces that sum. Selecting a BIOS by hand takes any file ending in .bin that is exactly 524288 bytes, so the other images of the group fill the slot without being recognised by name or by sum." + note: "The only image the code identifies, and it identifies it by a checksum of its own rather than by a hash it spells out: the 524288 bytes summed as signed bytes, accepted at 85958. The md5 and sha1 here are the dump that reproduces that sum. Selecting a BIOS by hand takes any file ending in .bin that is exactly 524288 bytes, so any other PlayStation image of that size fills the slot without being recognised by name or by sum." source_ref: "Main.java:4159-4175, Main.java:5919, Main.java:25217, res/raw/fpse.ini BiosName" - name: LibPS.exe diff --git a/emulators/pizza-boy-sc.yml b/emulators/pizza-boy-sc.yml index 734be75ef..2d5558357 100644 --- a/emulators/pizza-boy-sc.yml +++ b/emulators/pizza-boy-sc.yml @@ -85,12 +85,17 @@ notes: | box art, cheat, layout, sticker and per-ROM databases are created by their own CREATE TABLE. +groups: + megacd-boot-rom: + required: true + mastersystem-boot-rom: + required: false + files: - name: bios_CD_U.bin system: sega-megacd - required: true + group: megacd-boot-rom agnostic: true - selection_slot: "megacd-boot-rom" size: 131072 validation: [size] description: "Sega CD boot ROM" @@ -114,9 +119,8 @@ files: - name: bios_CD_E.bin system: sega-megacd - required: true + group: megacd-boot-rom agnostic: true - selection_slot: "megacd-boot-rom" size: 131072 validation: [size] description: "Mega CD boot ROM" @@ -127,9 +131,8 @@ files: - name: bios_CD_J.bin system: sega-megacd - required: true + group: megacd-boot-rom agnostic: true - selection_slot: "megacd-boot-rom" size: 131072 validation: [size] description: "Mega CD boot ROM (Japan)" @@ -140,9 +143,8 @@ files: - name: bios_U.sms system: sega-mastersystem - required: false + group: mastersystem-boot-rom agnostic: true - selection_slot: "mastersystem-boot-rom" description: "Master System boot ROM" note: >- Picked and stored like the Mega CD image and handed to the machine as a path. @@ -155,9 +157,8 @@ files: - name: bios_E.sms system: sega-mastersystem - required: false + group: mastersystem-boot-rom agnostic: true - selection_slot: "mastersystem-boot-rom" description: "Master System boot ROM (Europe)" note: "Same slot and same absence of tests as the other Master System boot ROMs." source_ref: "Pizza Boy SC Basic 1.2.14 libsc.so frontend/android/src/lib.rs:590, @@ -165,9 +166,8 @@ files: - name: bios_J.sms system: sega-mastersystem - required: false + group: mastersystem-boot-rom agnostic: true - selection_slot: "mastersystem-boot-rom" description: "Master System boot ROM (Japan)" note: "Same slot and same absence of tests as the other Master System boot ROMs." source_ref: "Pizza Boy SC Basic 1.2.14 libsc.so frontend/android/src/lib.rs:590,