mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-14 05:53:47 -06:00
feat: fill a core option default by model
RetroDECK sets LRPS2's pcsx2_bios to scph10000.bin. The record names the frontend, so the core is the one whose entry reads that setting. Its entry is a placeholder, and the name spells a model: the held dump the catalogues describe as SCPH-10000 goes in under that name in the entry's directory, primaries first, then the earliest Redump revision (ps2-0100j-20000117).
This commit is contained in:
1 parent
09fe506281
commit
8114f48e30
3 files changed
+179
-9
No files matched your search
@@ -50,7 +50,15 @@ from common import (
|
||||
select_emulator_profiles,
|
||||
)
|
||||
from hashing import parse_hash_list
|
||||
from launchers import LauncherLayer, declared_destinations, held_record, set_by, suffixed_stem
|
||||
from launchers import (
|
||||
LauncherLayer,
|
||||
catalogued_model_files,
|
||||
declared_destinations,
|
||||
held_record,
|
||||
set_by,
|
||||
setting_readers,
|
||||
suffixed_stem,
|
||||
)
|
||||
from launchers import _directories as launcher_directories
|
||||
from launchers import _names as launcher_names
|
||||
from validation import (
|
||||
@@ -1180,6 +1188,11 @@ def launcher_reads_not_held(
|
||||
for part in str(record.get("emulator", "")).split(",")
|
||||
for key in LauncherLayer._profiles_named(part.strip(), profiles, every_profile)
|
||||
})
|
||||
if not readers:
|
||||
# RetroDECK names its frontend and the core option names the core.
|
||||
readers = setting_readers(
|
||||
str(record.get("passes_to") or ""), profiles, every_profile
|
||||
)
|
||||
if emulator and emulator not in readers:
|
||||
continue
|
||||
folds = bool(record.get("case_insensitive") or config.get("case_insensitive_fs"))
|
||||
@@ -1187,12 +1200,26 @@ def launcher_reads_not_held(
|
||||
if record.get("kind") == "copies" and not launcher_directories(record, base):
|
||||
continue
|
||||
passes_to = str(record.get("passes_to") or "")
|
||||
if passes_to and any(
|
||||
set_by(entry, passes_to) and holder._entry_held(entry)
|
||||
setting_entries = [
|
||||
entry
|
||||
for key in readers
|
||||
for entry in profiles[key].get("files") or []
|
||||
):
|
||||
if passes_to and set_by(entry, passes_to)
|
||||
]
|
||||
if any(holder._entry_held(entry) for entry in setting_entries):
|
||||
continue
|
||||
# A placeholder read from the setting takes the held dump the
|
||||
# catalogues name after the model the launcher's name spells.
|
||||
by_model = {
|
||||
name
|
||||
for name in names
|
||||
for entry in setting_entries
|
||||
if "<" in str(entry.get("name", ""))
|
||||
and any(
|
||||
name_match_size_ok(entry, record_.get("size"))
|
||||
for record_ in catalogued_model_files(name, db)
|
||||
)
|
||||
}
|
||||
if record.get("kind") == "names_searched" and any(
|
||||
(found := holder.held(name)) and (found[1] or folds) for name in names
|
||||
):
|
||||
@@ -1216,7 +1243,7 @@ def launcher_reads_not_held(
|
||||
}
|
||||
for name in names:
|
||||
found = holder.held(name)
|
||||
if found and (found[1] or folds):
|
||||
if found and (found[1] or folds) or name in by_model:
|
||||
continue
|
||||
key = name.casefold() if folds else name
|
||||
row = by_key.get(key)
|
||||
|
||||
+112
-4
@@ -112,6 +112,49 @@ def suffixed_stem(name: str) -> str | None:
|
||||
return stem if "." in stem else None
|
||||
|
||||
|
||||
_MODEL = re.compile(r"\b([A-Z]{2,5})-?(\d{3,6}[A-Z]?)\b")
|
||||
|
||||
|
||||
def catalogued_model_files(name: str, db: dict) -> list[dict]:
|
||||
"""Held files a dump catalogue describes as the model a launcher name spells.
|
||||
|
||||
RetroDECK sets LRPS2's pcsx2_bios option to scph10000.bin, a name no code
|
||||
hashes: the SCPH-10000 is the model whose BIOS it means. Redump and TOSEC
|
||||
describe two revisions for it. Primaries come before .variants copies,
|
||||
then the Redump name, which carries the version (ps2-0100j before
|
||||
ps2-0101j), then the No-Intro and TOSEC names.
|
||||
"""
|
||||
stem = name.rsplit("/", 1)[-1].rsplit(".", 1)[0].casefold()
|
||||
if not re.fullmatch(r"[a-z]{2,5}\d{3,6}[a-z]?", stem):
|
||||
return []
|
||||
found = []
|
||||
for record in db.get("files", {}).values():
|
||||
catalogues = record.get("provenance") or {}
|
||||
models = {
|
||||
(a + b).casefold()
|
||||
for entry in catalogues.values()
|
||||
for a, b in _MODEL.findall(str(entry.get("description") or ""))
|
||||
}
|
||||
if stem in models:
|
||||
names = [
|
||||
str((catalogues.get(source) or {}).get("name") or "")
|
||||
for source in ("redump", "no-intro", "tosec")
|
||||
]
|
||||
found.append(("/.variants/" in str(record.get("path", "")), names, record))
|
||||
return [record for *_key, record in sorted(found, key=lambda f: (f[0], f[1]))]
|
||||
|
||||
|
||||
def setting_readers(passes_to: str, profiles: dict, keys: Iterable[str]) -> list[str]:
|
||||
"""The profiles with an entry read from the setting a launcher writes."""
|
||||
if not passes_to:
|
||||
return []
|
||||
return sorted(
|
||||
key for key in keys
|
||||
if (profile := profiles.get(key)) is not None
|
||||
and any(set_by(entry, passes_to) for entry in profile.get("files") or [])
|
||||
)
|
||||
|
||||
|
||||
def set_by(entry: dict, passes_to: str) -> bool:
|
||||
"""Whether a launcher hands its file to the setting a profile entry is read from."""
|
||||
key = str(entry.get("config_key") or "")
|
||||
@@ -451,12 +494,30 @@ class LauncherLayer:
|
||||
self._by_name_folded.setdefault(name.lower(), []).extend(sha1s)
|
||||
self._files = db.get("files", {})
|
||||
self._owners: dict[tuple[str, bool], list[LauncherPlace]] = {}
|
||||
self._placeholders: dict[tuple[str, bool], list[tuple[dict, dict]]] = {}
|
||||
relevant = set(relevant)
|
||||
for place in self.places:
|
||||
standalone = place.build == "standalone"
|
||||
for emulator in (e.strip() for e in place.emulator.split(",")):
|
||||
for key in self._profiles_named(emulator, profiles, relevant):
|
||||
self._owners.setdefault((key, standalone), []).append(place)
|
||||
owners = {
|
||||
key
|
||||
for emulator in (e.strip() for e in place.emulator.split(","))
|
||||
for key in self._profiles_named(emulator, profiles, relevant)
|
||||
}
|
||||
# RetroDECK names its frontend, retroarch, and the core option it
|
||||
# writes, pcsx2_bios, names the core.
|
||||
if not owners:
|
||||
owners = set(setting_readers(place.passes_to, profiles, relevant))
|
||||
for key in sorted(owners):
|
||||
self._owners.setdefault((key, standalone), []).append(place)
|
||||
# A placeholder entry stands for every file a scan finds and
|
||||
# is no gap, so the report never offers it: the setting a
|
||||
# launcher writes still reads one file of it.
|
||||
for entry in profiles[key].get("files") or []:
|
||||
if "<" in str(entry.get("name", "")) and set_by(entry, place.passes_to):
|
||||
self._placeholders.setdefault((key, standalone), []).append(
|
||||
(entry, _placeholder_report(key, profiles[key], entry))
|
||||
)
|
||||
self._db = db
|
||||
|
||||
@staticmethod
|
||||
def _profiles_named(name: str, profiles: dict, relevant: set[str]) -> list[str]:
|
||||
@@ -568,6 +629,8 @@ class LauncherLayer:
|
||||
by_layout: dict[tuple[str, bool], list[tuple[dict, dict]]] = {}
|
||||
for emulator, standalone, entry, report in candidates:
|
||||
by_layout.setdefault((emulator, standalone), []).append((entry, report))
|
||||
for layout, members in self._placeholders.items():
|
||||
by_layout.setdefault(layout, []).extend(members)
|
||||
placed: set[str] = set()
|
||||
for (emulator, standalone), members in sorted(by_layout.items()):
|
||||
for place in self.places_for(emulator, standalone):
|
||||
@@ -627,7 +690,31 @@ class LauncherLayer:
|
||||
):
|
||||
return True
|
||||
if not place.anchored:
|
||||
continue
|
||||
# A setting holds a name the core resolves in the directory
|
||||
# of the entry read from it: LRPS2 joins pcsx2_bios under
|
||||
# pcsx2/bios.
|
||||
setting = [
|
||||
(candidate, report) for candidate, report in matched
|
||||
if set_by(candidate.entry, place.passes_to)
|
||||
]
|
||||
directory = next(
|
||||
(
|
||||
str(c.entry.get("path") or "").rpartition("/")[0]
|
||||
for c, _report in setting
|
||||
if "/" in str(c.entry.get("path") or "")
|
||||
),
|
||||
None,
|
||||
)
|
||||
if directory is None:
|
||||
continue
|
||||
path = f"{directory}/{name}"
|
||||
if place.fold(path) in folded_reported:
|
||||
return True
|
||||
source = self._content_for(place, name, key, setting, resolve_held)
|
||||
if source is None or place.fold(path) in placed:
|
||||
continue
|
||||
placed.add(place.fold(path))
|
||||
return _launcher_entry(source[0], name, path, source[1], place)
|
||||
# The emulator as this platform installs it brings the file: the
|
||||
# place is filled, nothing is packed (ROCKNIX's xemu hdd image).
|
||||
if any(key in candidate.names and report.get("bundled")
|
||||
@@ -676,9 +763,30 @@ class LauncherLayer:
|
||||
record = resolve_held(report)
|
||||
if record is not None:
|
||||
return report, record
|
||||
for candidate, report in matched:
|
||||
if key not in candidate.names or not set_by(candidate.entry, place.passes_to):
|
||||
continue
|
||||
for record in catalogued_model_files(name, self._db):
|
||||
if name_match_size_ok(candidate.entry, record.get("size")):
|
||||
return report, record
|
||||
return None
|
||||
|
||||
|
||||
def _placeholder_report(key: str, profile: dict, entry: dict) -> dict:
|
||||
"""The report entry of a placeholder, which the gap report never builds."""
|
||||
return {
|
||||
"profile": key,
|
||||
"emulator": profile.get("emulator", key),
|
||||
"name": entry.get("name", ""),
|
||||
"path": entry.get("path") or entry.get("name", ""),
|
||||
"system": entry.get("system"),
|
||||
"systems": profile.get("systems"),
|
||||
"required": entry.get("required", False),
|
||||
"category": entry.get("category"),
|
||||
"in_repo": False,
|
||||
}
|
||||
|
||||
|
||||
def _launcher_entry(
|
||||
report: dict, name: str, path: str, record: dict, place: LauncherPlace
|
||||
) -> dict:
|
||||
|
||||
@@ -289,6 +289,41 @@ class LauncherLayerTest(unittest.TestCase):
|
||||
self.assertEqual(added["kick.A500.rom"]["md5"], _md5(CONTENT["core.bin"]))
|
||||
self.assertIsNone(suffixed_stem("scph1001.bin"))
|
||||
|
||||
def test_a_core_option_names_the_dump_of_a_model(self) -> None:
|
||||
"""RetroDECK sets LRPS2's pcsx2_bios to scph10000.bin. LRPS2's entry
|
||||
is a placeholder for every BIOS its scan finds, and the catalogues
|
||||
name two revisions of the SCPH-10000: the earlier goes in."""
|
||||
older, newer = b"S" * 64, b"T" * 64
|
||||
for data, catalogue in ((newer, "ps2-0101j-20000217.bin"),
|
||||
(older, "ps2-0100j-20000117.bin")):
|
||||
sha1 = hashlib.sha1(data).hexdigest()
|
||||
path = os.path.join(self.bios_dir, catalogue)
|
||||
with open(path, "wb") as fh:
|
||||
fh.write(data)
|
||||
self.db["files"][sha1] = {
|
||||
"path": path, "name": catalogue, "md5": _md5(data), "sha1": sha1,
|
||||
"size": len(data), "crc32": "",
|
||||
"provenance": {"redump": {"name": catalogue,
|
||||
"description": "SCPH-10000 (Version 5.0)"}},
|
||||
}
|
||||
profile = {
|
||||
"emulator": "OptEmu", "type": "libretro", "systems": ["test-sys"],
|
||||
"files": [{"name": "<bios>.bin", "path": "ps2/bios/<bios>.bin", "required": True,
|
||||
"min_size": 32, "max_size": 64, "validation": ["size"],
|
||||
"config_key": "emu_bios"}],
|
||||
}
|
||||
with open(os.path.join(self.emulators_dir, "optemu.yml"), "w") as fh:
|
||||
yaml.dump(profile, fh)
|
||||
self.config["launchers"].append({
|
||||
"emulator": "retroarch", "build": "libretro", "kind": "core_option",
|
||||
"names": ["scph10000.bin"], "passes_to": "core option emu_bios",
|
||||
"source_ref": "core-options.cfg:515",
|
||||
})
|
||||
_emulator_profiles_cache.clear()
|
||||
self.profiles = load_emulator_profiles(self.emulators_dir)
|
||||
added = {u["path"]: u for u in self._report() if u.get("launcher")}
|
||||
self.assertEqual(added["ps2/bios/scph10000.bin"]["md5"], _md5(older))
|
||||
|
||||
def test_the_default_name_a_launcher_replaces_is_not_packed(self) -> None:
|
||||
"""EmuDeck passes Complex_4627v1.03.bin to xemu, which then never
|
||||
opens Complex_4627.bin: the pack carried both, one read by nobody."""
|
||||
|
||||
Reference in new issue
Block a user