mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-12 21:13:49 -06:00
chore: merge maker folder name matches
This commit is contained in:
commit
1008b4b0f5
14 files changed
+2908
-15185
No files matched your search
@@ -90,6 +90,71 @@ def _casefold_name_index(by_name: dict) -> dict[str, list[str]]:
|
||||
return folded
|
||||
|
||||
|
||||
# bios/Other/<emulator> and bios/Arcade/<emulator or board> file a copy by
|
||||
# who loads it, not by machine.
|
||||
BUCKET_FOLDERS = frozenset({"Other", "Arcade"})
|
||||
|
||||
|
||||
def names_manufacturer(sys_id: str, manufacturer: str) -> bool:
|
||||
"""Whether a system id names a maker folder: nec-pc-88 names NEC."""
|
||||
slug = manufacturer.lower().replace(" ", "-")
|
||||
return sys_id == slug or sys_id.startswith(f"{slug}-")
|
||||
|
||||
|
||||
def _maker_folder(path: str, bios_dir: str) -> str | None:
|
||||
"""The bios/<maker>/ folder a collection path is filed under."""
|
||||
prefix = f"{bios_dir.rstrip('/')}/"
|
||||
if not path.startswith(prefix):
|
||||
return None
|
||||
parts = path[len(prefix):].split("/")
|
||||
return parts[0] if len(parts) >= 2 else None
|
||||
|
||||
|
||||
# One slot, keyed like the casefold view: the files dict, its size, the folders.
|
||||
_maker_folders_cache: list[tuple[dict, int, frozenset[str]]] = []
|
||||
|
||||
|
||||
def _maker_folders(db: dict) -> frozenset[str]:
|
||||
"""Every maker folder the collection files a copy under."""
|
||||
files_db = db.get("files", {})
|
||||
if _maker_folders_cache:
|
||||
source, size, folders = _maker_folders_cache[0]
|
||||
if source is files_db and size == len(files_db):
|
||||
return folders
|
||||
bios_dir = str(db.get("bios_dir") or "bios")
|
||||
folders = frozenset(
|
||||
folder
|
||||
for entry in files_db.values()
|
||||
if (folder := _maker_folder(str(entry.get("path", "")), bios_dir))
|
||||
and folder not in BUCKET_FOLDERS
|
||||
)
|
||||
_maker_folders_cache[:] = [(files_db, len(files_db), folders)]
|
||||
return folders
|
||||
|
||||
|
||||
def system_makers(file_entry: dict, db: dict) -> frozenset[str]:
|
||||
"""The maker folders a platform entry's system names.
|
||||
|
||||
Only a platform's own entry is asked: one that names an owning emulator
|
||||
is placed by that emulator's tree. Empty when the system names no maker
|
||||
(pc88, arcade), so nothing is narrowed there.
|
||||
"""
|
||||
sys_id = str(file_entry.get("platform_system") or "")
|
||||
if not sys_id or _owner_tokens(file_entry):
|
||||
return frozenset()
|
||||
return frozenset(
|
||||
folder for folder in _maker_folders(db) if names_manufacturer(sys_id, folder)
|
||||
)
|
||||
|
||||
|
||||
def other_maker(path: str, makers: frozenset[str], db: dict) -> bool:
|
||||
"""Whether a path is filed under a maker the entry's system does not name."""
|
||||
if not makers:
|
||||
return False
|
||||
folder = _maker_folder(path, str(db.get("bios_dir") or "bios"))
|
||||
return folder is not None and folder not in BUCKET_FOLDERS and folder not in makers
|
||||
|
||||
|
||||
def name_match_size_ok(file_entry: dict, candidate_size: int | None) -> bool:
|
||||
"""Whether a candidate found by name may be the file the entry describes.
|
||||
|
||||
@@ -357,6 +422,19 @@ def load_platform_config(platform_name: str, platforms_dir: str = "platforms") -
|
||||
for sys_id, system in config.get("systems", {}).items()
|
||||
}
|
||||
|
||||
# Each entry carries the system that declares it: a hashless name match is
|
||||
# narrowed to the maker folder that system names (resolve_local_file).
|
||||
# Fresh dicts, since a YAML anchor can share one entry between systems.
|
||||
config["systems"] = {
|
||||
sys_id: {
|
||||
**system,
|
||||
"files": [{**f, "platform_system": sys_id} for f in system["files"]],
|
||||
}
|
||||
if isinstance(system, dict) and system.get("files")
|
||||
else system
|
||||
for sys_id, system in config.get("systems", {}).items()
|
||||
}
|
||||
|
||||
# The registry key, which per-platform profile fields (installed_on,
|
||||
# not_bundled_on) name: the display name differs (MiSTer FPGA).
|
||||
config["_key"] = platform_name
|
||||
@@ -1573,6 +1651,10 @@ def resolve_local_file(
|
||||
# hash and path steps above run first, so collecting the real bytes makes
|
||||
# the entry resolve.
|
||||
unsourceable = bool(file_entry.get("unsourceable")) or own_file_absent
|
||||
# A platform's own hashless entry whose system names a maker takes no copy
|
||||
# filed under another maker: AmberELEC's nec-pc-88 DISK.ROM, which QUASI88
|
||||
# reads as the 2 KB PC-88 disk ROM, got the 16 KB MSX one by name.
|
||||
makers = frozenset() if has_strong_hash else system_makers(file_entry, db)
|
||||
|
||||
if not has_strong_hash and not unsourceable:
|
||||
candidates = []
|
||||
@@ -1637,6 +1719,26 @@ def resolve_local_file(
|
||||
continue
|
||||
if os.path.exists(path) and path not in candidates:
|
||||
candidates.append(path)
|
||||
if makers and candidates:
|
||||
candidates = [p for p in candidates if not other_maker(p, makers, db)]
|
||||
if not candidates:
|
||||
# The name belongs to another maker's file here. An emulator
|
||||
# of this system may still read it as an alias of one of its
|
||||
# own files, an alias by_name leaves out because a primary
|
||||
# already carries the name: QUASI88 reads DISK.ROM as
|
||||
# n88sub.rom.
|
||||
shadowed = db.get("indexes", {}).get("by_shadowed_alias", {})
|
||||
for try_name in own_names:
|
||||
for match_sha1 in shadowed.get(try_name, []):
|
||||
path = files_db.get(match_sha1, {}).get("path", "")
|
||||
if (
|
||||
_maker_folder(path, str(db.get("bios_dir") or "bios"))
|
||||
in makers
|
||||
and _size_ok(match_sha1)
|
||||
and os.path.exists(path)
|
||||
and path not in candidates
|
||||
):
|
||||
candidates.append(path)
|
||||
if candidates:
|
||||
if zipped_file:
|
||||
candidates = [p for p in candidates if ".zip" in os.path.basename(p)]
|
||||
@@ -1701,6 +1803,8 @@ def resolve_local_file(
|
||||
|
||||
if zipped_file:
|
||||
candidates = [c for c in candidates if ".zip" in os.path.basename(c[0])]
|
||||
# Another maker's file is no mismatch of this one, it is another file.
|
||||
candidates = [c for c in candidates if not other_maker(c[0], makers, db)]
|
||||
# A size the emulator itself verifies settles a name collision, so a
|
||||
# candidate that fits is preferred over one that does not: "ROM" answers
|
||||
# both an Apple IIgs image and a Macintosh one. It ranks rather than
|
||||
|
||||
+39
-2
@@ -294,6 +294,28 @@ def build_indexes(files: dict, aliases: dict) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def shadowed_alias_index(
|
||||
shadowed: list[tuple[str, set[str], list[str]]], by_name: dict
|
||||
) -> dict[str, list[str]]:
|
||||
"""Profile aliases that a primary's name keeps out of by_name.
|
||||
|
||||
QUASI88 reads DISK.ROM as n88sub.rom, but DISK.ROM is the MSX disk ROM's
|
||||
own name, so by_name never points it at the PC-88 file. The resolver reads
|
||||
this index only where the system names a maker and no copy filed under
|
||||
that maker answers the name. An entry with no hash designates the content
|
||||
its own name holds alone in by_name.
|
||||
"""
|
||||
index: dict[str, set[str]] = {}
|
||||
for entry_name, matched, taken in shadowed:
|
||||
held = by_name.get(entry_name, [])
|
||||
designated = matched or (set(held) if len(held) == 1 else set())
|
||||
for alias in taken:
|
||||
fresh = designated - set(by_name.get(alias, []))
|
||||
if fresh:
|
||||
index.setdefault(alias, set()).update(fresh)
|
||||
return {alias: sorted(sha1s) for alias, sha1s in sorted(index.items())}
|
||||
|
||||
|
||||
def load_cache(cache_path: str) -> dict:
|
||||
"""Load cache file if it exists."""
|
||||
try:
|
||||
@@ -429,8 +451,9 @@ def main() -> int:
|
||||
if preserved:
|
||||
print(f" Preserved {preserved} large file entries from existing database")
|
||||
|
||||
shadowed: list[tuple[str, set[str], list[str]]] = []
|
||||
try:
|
||||
platform_aliases = _collect_all_aliases(files, aliases)
|
||||
platform_aliases = _collect_all_aliases(files, aliases, shadowed)
|
||||
except UnreadableSource as exc:
|
||||
for source in exc.sources:
|
||||
print(f"Error: cannot read {source}", file=sys.stderr)
|
||||
@@ -446,6 +469,7 @@ def main() -> int:
|
||||
provenance_counts = annotate_provenance(files, snapshots)
|
||||
|
||||
indexes = build_indexes(files, aliases)
|
||||
indexes["by_shadowed_alias"] = shadowed_alias_index(shadowed, indexes["by_name"])
|
||||
total_size = sum(entry["size"] for entry in files.values())
|
||||
|
||||
database = {
|
||||
@@ -508,11 +532,17 @@ class UnreadableSource(Exception):
|
||||
self.sources = sources
|
||||
|
||||
|
||||
def _collect_all_aliases(files: dict, copies: dict | None = None) -> dict:
|
||||
def _collect_all_aliases(
|
||||
files: dict,
|
||||
copies: dict | None = None,
|
||||
shadowed: list[tuple[str, set[str], list[str]]] | None = None,
|
||||
) -> dict:
|
||||
"""Collect alternate filenames from platform YAMLs, core-info, and known aliases.
|
||||
|
||||
Registers alternate names so generate_pack can resolve files stored under different names.
|
||||
``copies`` maps a sha1 to the other files the scan found with that content.
|
||||
``shadowed`` receives, per profile entry, its name, the contents its hashes
|
||||
designate and the aliases a primary's name already carries.
|
||||
"""
|
||||
md5_to_sha1 = {}
|
||||
name_to_sha1 = {}
|
||||
@@ -635,6 +665,13 @@ def _collect_all_aliases(files: dict, copies: dict | None = None) -> dict:
|
||||
entry_aliases.insert(0, entry_name)
|
||||
if not entry_aliases:
|
||||
continue
|
||||
if shadowed is not None:
|
||||
taken = [
|
||||
alias for alias in entry_aliases
|
||||
if alias != entry_name and alias in named_primaries
|
||||
]
|
||||
if taken:
|
||||
shadowed.append((entry_name, set(matched), taken))
|
||||
if not matched and len(holders.get(entry_name, ())) == 1:
|
||||
# A name carried by several contents names none of them:
|
||||
# quasi88's disk.rom aliases went to whichever
|
||||
|
||||
+19
-14
@@ -40,6 +40,8 @@ from typing import TypeVar
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from common import (
|
||||
BUCKET_FOLDERS,
|
||||
names_manufacturer,
|
||||
upstream_profile_index,
|
||||
build_zip_contents_index,
|
||||
GAME_DATA_TOPS,
|
||||
@@ -219,21 +221,10 @@ def _copy_places(db: dict) -> dict[str, set[tuple[str, str, str]]]:
|
||||
return places
|
||||
|
||||
|
||||
# bios/Other/<emulator> and bios/Arcade/<emulator or board> file a copy by
|
||||
# who loads it, not by machine. A copy also filed under a machine's maker
|
||||
# places the system there.
|
||||
_BUCKET_FOLDERS = frozenset({"Other", "Arcade"})
|
||||
|
||||
|
||||
def _slug(manufacturer: str) -> str:
|
||||
return manufacturer.lower().replace(" ", "-")
|
||||
|
||||
|
||||
def _names_manufacturer(sys_id: str, manufacturer: str) -> bool:
|
||||
slug = _slug(manufacturer)
|
||||
return sys_id == slug or sys_id.startswith(f"{slug}-")
|
||||
|
||||
|
||||
def _copies_by_path(db: dict) -> dict[str, set[tuple[str, str, str]]]:
|
||||
"""The copy places of each content, keyed by the path the database keeps."""
|
||||
files = db.get("files", {})
|
||||
@@ -276,13 +267,13 @@ def _system_folder(
|
||||
declared = str(fe.get("name", "")).casefold()
|
||||
named = {
|
||||
(m, c) for m, c, base in held
|
||||
if base.casefold() == declared or _names_manufacturer(sys_id, m)
|
||||
if base.casefold() == declared or names_manufacturer(sys_id, m)
|
||||
}
|
||||
for place in named or {(m, c) for m, c, _ in held}:
|
||||
votes[place] = votes.get(place, 0) + 1
|
||||
if votes:
|
||||
own = [place for place in votes if _names_manufacturer(sys_id, place[0])]
|
||||
makers = [place for place in votes if place[0] not in _BUCKET_FOLDERS]
|
||||
own = [place for place in votes if names_manufacturer(sys_id, place[0])]
|
||||
makers = [place for place in votes if place[0] not in BUCKET_FOLDERS]
|
||||
candidates = own or makers or list(votes)
|
||||
return max(sorted(candidates), key=lambda place: votes[place])
|
||||
for fe in files:
|
||||
@@ -1044,6 +1035,20 @@ def _split_contract(config: dict) -> tuple[dict, dict]:
|
||||
"""
|
||||
allowed = _contract_keys()
|
||||
contract = {key: value for key, value in config.items() if key in allowed}
|
||||
# platform_system is the resolver's copy of the key each entry sits under.
|
||||
if isinstance(contract.get("systems"), dict):
|
||||
contract["systems"] = {
|
||||
sys_id: {
|
||||
**system,
|
||||
"files": [
|
||||
{k: v for k, v in f.items() if k != "platform_system"}
|
||||
for f in system["files"]
|
||||
],
|
||||
}
|
||||
if isinstance(system, dict) and system.get("files")
|
||||
else system
|
||||
for sys_id, system in contract["systems"].items()
|
||||
}
|
||||
registry = {key: value for key, value in config.items()
|
||||
if key not in allowed and key != "_key"}
|
||||
# The merged config gives a child its parent's release_pack: Lakka and
|
||||
|
||||
Reference in new issue
Block a user