mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-16 00:13:25 -05:00
feat: install only what the installed cores read
This commit is contained in:
1 parent
058ac673c4
commit
fbe432ccd0
23 files changed
+16680
-2185
No files matched your search
+1
-1
@@ -8,7 +8,7 @@ param(
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
|
||||
$defaultInstallSha256 = "95e9cf9c274d3ccf5a305ee4a32e74c5c173177b7cf012069835a096e7ff2b35"
|
||||
$defaultInstallSha256 = "0aa7b10a9c6f295366f5d22df012d7205af4ccf4cc9d9cdc9cd90872cc4d6bf3"
|
||||
$maximumInstallerBytes = 2MB
|
||||
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
|
||||
$temporary = $null
|
||||
|
||||
+332
-61
@@ -427,6 +427,35 @@ def _expand_retroarch_path(value: str, app_dir: Path) -> Path:
|
||||
return Path(os.path.expandvars(os.path.expanduser(value)))
|
||||
|
||||
|
||||
def _retroarch_cfg_dir(
|
||||
cfg_path: Path, key: str, app_dir: Path, default: str
|
||||
) -> Path | None:
|
||||
"""A directory setting of retroarch.cfg, as RetroArch expands it.
|
||||
|
||||
A value left empty or at `default` is the directory RetroArch falls back
|
||||
on, `default` under app_dir. None when the config is unreadable or does
|
||||
not set the key.
|
||||
"""
|
||||
try:
|
||||
lines = cfg_path.read_text(encoding="utf-8", errors="replace").splitlines()
|
||||
except OSError:
|
||||
return None
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
name, sep, value = line.partition("=")
|
||||
if sep and name.strip() == key:
|
||||
value = value.strip().strip('"').strip("'")
|
||||
if not value or value == "default":
|
||||
return app_dir / default
|
||||
return _expand_retroarch_path(value, app_dir)
|
||||
return None
|
||||
|
||||
|
||||
# System directory -> the retroarch.cfg it was read from and the directory
|
||||
# ':' expands to there, for finding the cores of the same installation.
|
||||
_RETROARCH_CONFIGS: dict[Path, tuple[Path, Path]] = {}
|
||||
|
||||
|
||||
def _parse_retroarch_system_dir(
|
||||
cfg_path: Path, app_dir: Path | None = None
|
||||
) -> Path | None:
|
||||
@@ -439,18 +468,10 @@ def _parse_retroarch_system_dir(
|
||||
if not cfg_path.exists():
|
||||
return None
|
||||
app_dir = app_dir or cfg_path.parent
|
||||
try:
|
||||
for line in cfg_path.read_text(encoding="utf-8", errors="replace").splitlines():
|
||||
line = line.strip()
|
||||
if line.startswith("system_directory"):
|
||||
_, _, value = line.partition("=")
|
||||
value = value.strip().strip('"').strip("'")
|
||||
if not value or value == "default":
|
||||
return app_dir / "system"
|
||||
return _expand_retroarch_path(value, app_dir)
|
||||
except OSError:
|
||||
pass
|
||||
return None
|
||||
system_dir = _retroarch_cfg_dir(cfg_path, "system_directory", app_dir, "system")
|
||||
if system_dir is not None:
|
||||
_RETROARCH_CONFIGS.setdefault(system_dir, (cfg_path, app_dir))
|
||||
return system_dir
|
||||
|
||||
|
||||
def _shell_unquote(value: str) -> str:
|
||||
@@ -920,6 +941,69 @@ def detect_platforms(os_type: str) -> list[tuple[str, Path]]:
|
||||
return found
|
||||
|
||||
|
||||
# A libretro core as RetroArch loads it: <name>_libretro.so|dll|dylib, and
|
||||
# <name>_libretro_android.so on Android.
|
||||
_CORE_FILE_RE = re.compile(
|
||||
r"^(?P<name>.+?)_libretro(?:_android)?\.(?:so|dll|dylib)$", re.IGNORECASE
|
||||
)
|
||||
CORE_PLATFORMS = ("retroarch", "lakka", "retropie")
|
||||
# Where these systems install cores whatever retroarch.cfg says: RetroPie
|
||||
# gives each package its own folder, Lakka keeps the image's cores and the
|
||||
# ones the online updater fetches apart.
|
||||
_FIXED_CORE_DIRS = {
|
||||
"retropie": (Path("/opt/retropie/libretrocores"),),
|
||||
"lakka": (Path("/usr/lib/libretro"), Path("/storage/cores"), Path("/tmp/cores")),
|
||||
}
|
||||
|
||||
|
||||
def core_dirs(plat: str, bios_path: Path) -> list[Path]:
|
||||
"""The directories the RetroArch writing to bios_path loads cores from.
|
||||
|
||||
The config detection read for this system directory names them in
|
||||
libretro_directory; without one, a retroarch.cfg beside the system
|
||||
directory is the portable layout. Unset, the setting is cores/ under
|
||||
RetroArch's own directory.
|
||||
"""
|
||||
found = _RETROARCH_CONFIGS.get(bios_path)
|
||||
if found is None and (bios_path.parent / "retroarch.cfg").is_file():
|
||||
found = (bios_path.parent / "retroarch.cfg", bios_path.parent)
|
||||
dirs: list[Path] = []
|
||||
if found is not None:
|
||||
cfg, app_dir = found
|
||||
dirs.append(
|
||||
_retroarch_cfg_dir(cfg, "libretro_directory", app_dir, "cores")
|
||||
or app_dir / "cores"
|
||||
)
|
||||
dirs.extend(_FIXED_CORE_DIRS.get(plat, ()))
|
||||
return list(dict.fromkeys(dirs))
|
||||
|
||||
|
||||
def installed_cores(dirs: list[Path]) -> list[str]:
|
||||
"""The cores present in these directories, by the name the buildbot uses.
|
||||
|
||||
A folder inside is looked into once, which is how RetroPie installs
|
||||
each package. An unreadable directory holds nothing that can be seen.
|
||||
"""
|
||||
names: set[str] = set()
|
||||
for directory in dirs:
|
||||
try:
|
||||
children = list(directory.iterdir())
|
||||
except OSError:
|
||||
continue
|
||||
for child in children:
|
||||
candidates = [child]
|
||||
if child.is_dir():
|
||||
try:
|
||||
candidates = list(child.iterdir())
|
||||
except OSError:
|
||||
continue
|
||||
for path in candidates:
|
||||
match = _CORE_FILE_RE.match(path.name)
|
||||
if match and path.is_file():
|
||||
names.add(match.group("name"))
|
||||
return sorted(names)
|
||||
|
||||
|
||||
def normalize_platform(name: str) -> str:
|
||||
"""Return the canonical platform id for user-supplied input."""
|
||||
plat = name.strip().lower()
|
||||
@@ -1021,6 +1105,48 @@ def _check_tags(entry: dict, field: str, where: str) -> None:
|
||||
raise ValueError(f"invalid {field} for {where}")
|
||||
|
||||
|
||||
# What an alternative replaces on the entry it stands in for.
|
||||
_COPY_FIELDS = ("sha1", "sha256", "size", "repo_path", "release_asset", "storage", "cores")
|
||||
|
||||
|
||||
def _check_download(entry: dict, where: str, dest: str) -> int:
|
||||
"""The size, hashes, source and cores of one downloadable copy; its size."""
|
||||
size = entry.get("size")
|
||||
if isinstance(size, bool) or not isinstance(size, int) or not (0 <= size <= MAX_DOWNLOAD_SIZE):
|
||||
raise ValueError(f"invalid size for {dest}")
|
||||
|
||||
sha1 = entry.get("sha1", "")
|
||||
sha256 = entry.get("sha256", "")
|
||||
if sha1 and (not isinstance(sha1, str) or not _SHA1_RE.fullmatch(sha1)):
|
||||
raise ValueError(f"invalid SHA1 for {dest}")
|
||||
if sha256 and (
|
||||
not isinstance(sha256, str) or not _SHA256_RE.fullmatch(sha256)
|
||||
):
|
||||
raise ValueError(f"invalid SHA256 for {dest}")
|
||||
if not sha1 and not sha256:
|
||||
raise ValueError(f"missing content hash for {dest}")
|
||||
|
||||
release_asset = entry.get("release_asset")
|
||||
repo_path = entry.get("repo_path")
|
||||
if release_asset:
|
||||
asset = _safe_relative_path(release_asset, f"{where}.release_asset")
|
||||
if len(asset.parts) != 1:
|
||||
raise ValueError(f"release asset must be a basename: {release_asset}")
|
||||
elif repo_path:
|
||||
source = _safe_relative_path(repo_path, f"{where}.repo_path")
|
||||
if source.parts[0] != "bios":
|
||||
raise ValueError(f"repo_path outside bios/: {repo_path}")
|
||||
else:
|
||||
raise ValueError(f"no download source for {dest}")
|
||||
|
||||
cores = entry.get("cores")
|
||||
if cores is not None and (
|
||||
not isinstance(cores, list) or not all(isinstance(core, str) for core in cores)
|
||||
):
|
||||
raise ValueError(f"invalid cores list for {dest}")
|
||||
return size
|
||||
|
||||
|
||||
def _validate_manifest(data: object, plat: str) -> dict:
|
||||
"""Validate the untrusted install-manifest boundary using stdlib only."""
|
||||
if not isinstance(data, dict):
|
||||
@@ -1035,6 +1161,7 @@ def _validate_manifest(data: object, plat: str) -> dict:
|
||||
|
||||
seen_destinations: set[str] = set()
|
||||
total_size = 0
|
||||
largest_total = 0
|
||||
for index, entry in enumerate(files):
|
||||
if not isinstance(entry, dict):
|
||||
raise ValueError(f"files[{index}] must be an object")
|
||||
@@ -1043,42 +1170,24 @@ def _validate_manifest(data: object, plat: str) -> dict:
|
||||
raise ValueError(f"duplicate manifest destination: {dest}")
|
||||
seen_destinations.add(dest)
|
||||
|
||||
size = entry.get("size")
|
||||
if isinstance(size, bool) or not isinstance(size, int) or not (0 <= size <= MAX_DOWNLOAD_SIZE):
|
||||
raise ValueError(f"invalid size for {dest}")
|
||||
size = _check_download(entry, f"files[{index}]", dest)
|
||||
total_size += size
|
||||
if total_size > MAX_TOTAL_DOWNLOAD_SIZE:
|
||||
alternatives = entry.get("alternatives", [])
|
||||
if not isinstance(alternatives, list) or len(alternatives) > MAX_SELECTION_TAGS:
|
||||
raise ValueError(f"invalid alternatives for {dest}")
|
||||
for alt_index, alternative in enumerate(alternatives):
|
||||
where = f"files[{index}].alternatives[{alt_index}]"
|
||||
if (
|
||||
not isinstance(alternative, dict)
|
||||
or not alternative.get("cores")
|
||||
or not set(alternative) <= set(_COPY_FIELDS)
|
||||
):
|
||||
raise ValueError(f"invalid alternative for {dest}")
|
||||
# Narrowing can swap in any copy: the cap holds for the largest.
|
||||
size = max(size, _check_download(alternative, where, dest))
|
||||
largest_total += size
|
||||
if largest_total > MAX_TOTAL_DOWNLOAD_SIZE:
|
||||
raise ValueError("manifest total size exceeds safety limit")
|
||||
|
||||
sha1 = entry.get("sha1", "")
|
||||
sha256 = entry.get("sha256", "")
|
||||
if sha1 and (not isinstance(sha1, str) or not _SHA1_RE.fullmatch(sha1)):
|
||||
raise ValueError(f"invalid SHA1 for {dest}")
|
||||
if sha256 and (
|
||||
not isinstance(sha256, str) or not _SHA256_RE.fullmatch(sha256)
|
||||
):
|
||||
raise ValueError(f"invalid SHA256 for {dest}")
|
||||
if not sha1 and not sha256:
|
||||
raise ValueError(f"missing content hash for {dest}")
|
||||
|
||||
release_asset = entry.get("release_asset")
|
||||
repo_path = entry.get("repo_path")
|
||||
if release_asset:
|
||||
asset = _safe_relative_path(release_asset, f"files[{index}].release_asset")
|
||||
if len(asset.parts) != 1:
|
||||
raise ValueError(f"release asset must be a basename: {release_asset}")
|
||||
elif repo_path:
|
||||
source = _safe_relative_path(repo_path, f"files[{index}].repo_path")
|
||||
if source.parts[0] != "bios":
|
||||
raise ValueError(f"repo_path outside bios/: {repo_path}")
|
||||
else:
|
||||
raise ValueError(f"no download source for {dest}")
|
||||
|
||||
cores = entry.get("cores")
|
||||
if cores is not None and (
|
||||
not isinstance(cores, list) or not all(isinstance(core, str) for core in cores)
|
||||
):
|
||||
raise ValueError(f"invalid cores list for {dest}")
|
||||
for field in SELECTION_FIELDS:
|
||||
_check_tags(entry, field, dest)
|
||||
|
||||
@@ -1127,6 +1236,19 @@ def _validate_manifest(data: object, plat: str) -> dict:
|
||||
):
|
||||
raise ValueError("manifest total_omitted does not match omitted_files")
|
||||
|
||||
for field in ("core_names", "system_cores"):
|
||||
mapping = data.get(field)
|
||||
if mapping is None:
|
||||
continue
|
||||
if not isinstance(mapping, dict) or len(mapping) > MAX_MANIFEST_FILES:
|
||||
raise ValueError(f"invalid {field}")
|
||||
for key in mapping:
|
||||
if not isinstance(key, str) or not 0 < len(key) <= 256:
|
||||
raise ValueError(f"invalid {field} key")
|
||||
_check_tags(mapping, key, field)
|
||||
if mapping[key] is None:
|
||||
raise ValueError(f"invalid {field} for {key}")
|
||||
|
||||
copies = data.get("standalone_copies", [])
|
||||
if not isinstance(copies, list) or len(copies) > 10_000:
|
||||
raise ValueError("invalid standalone_copies")
|
||||
@@ -1241,19 +1363,95 @@ def fetch_targets(plat: str) -> "dict | None":
|
||||
return None
|
||||
|
||||
|
||||
def _for_cores(entry: dict, chosen: set[str]) -> dict | None:
|
||||
"""The copy of a core file the chosen cores read, or None.
|
||||
|
||||
A destination holds one file. When several cores read it and disagree
|
||||
on its content, the manifest carries the first core's copy and the
|
||||
others as alternatives, in the order the pack builder tries them: a
|
||||
pack built without the first core carries the next one's copy.
|
||||
"""
|
||||
if chosen.intersection(entry.get("cores") or []):
|
||||
return entry
|
||||
for alternative in entry.get("alternatives", []):
|
||||
if chosen.intersection(alternative["cores"]):
|
||||
kept = {
|
||||
k: v for k, v in entry.items()
|
||||
if k not in _COPY_FIELDS and k != "alternatives"
|
||||
}
|
||||
kept.update((k, alternative[k]) for k in _COPY_FIELDS if k in alternative)
|
||||
return kept
|
||||
return None
|
||||
|
||||
|
||||
def _filter_by_target(
|
||||
files: list[dict], target_cores: list[str]
|
||||
) -> list[dict]:
|
||||
"""Keep files where cores is None or overlaps with target_cores."""
|
||||
"""Keep the platform's files, and the core files the given cores read."""
|
||||
chosen = set(target_cores)
|
||||
result: list[dict] = []
|
||||
target_set = set(target_cores)
|
||||
for f in files:
|
||||
cores = f.get("cores")
|
||||
if cores is None or any(c in target_set for c in cores):
|
||||
if f.get("cores") is None:
|
||||
result.append(f)
|
||||
continue
|
||||
kept = _for_cores(f, chosen)
|
||||
if kept is not None:
|
||||
result.append(kept)
|
||||
return result
|
||||
|
||||
|
||||
def designated_cores(manifest: dict, names: list[str]) -> set[str]:
|
||||
"""The cores a list of names designates, the way the pack builder reads it.
|
||||
|
||||
A target list, or a core found installed, names a core the buildbot's
|
||||
way (`mednafen_psx`); a file names the profile that reads it
|
||||
(`beetle_psx`). `core_names` gives each profile its other names.
|
||||
"""
|
||||
wanted = set(names)
|
||||
chosen = set(wanted)
|
||||
for profile, aliases in (manifest.get("core_names") or {}).items():
|
||||
if wanted.intersection(aliases):
|
||||
chosen.add(profile)
|
||||
return chosen
|
||||
|
||||
|
||||
def narrow_to_cores(
|
||||
manifest: dict,
|
||||
names: list[str],
|
||||
files: list[dict] | None = None,
|
||||
omitted: list[dict] | None = None,
|
||||
) -> tuple[list[dict], list[dict]]:
|
||||
"""Keep what the named cores read, as a pack built for them carries.
|
||||
|
||||
A file a core loads stays when that core is named. A file of the
|
||||
platform's own list stays when one of its systems does: a system goes
|
||||
when none of the cores `system_cores` gives it is named, and a system
|
||||
it does not list stays whatever is named. A manifest without the two
|
||||
maps keeps the platform's list whole and matches cores by name.
|
||||
"""
|
||||
cores = designated_cores(manifest, names)
|
||||
reach = manifest.get("system_cores") or {}
|
||||
|
||||
def kept(entries: list[dict]) -> list[dict]:
|
||||
result: list[dict] = []
|
||||
for entry in entries:
|
||||
if entry.get("cores") is not None:
|
||||
copy = _for_cores(entry, cores)
|
||||
if copy is not None:
|
||||
result.append(copy)
|
||||
continue
|
||||
systems = entry_systems(entry)
|
||||
if not systems or any(
|
||||
s not in reach or cores.intersection(reach[s]) for s in systems
|
||||
):
|
||||
result.append(entry)
|
||||
return result
|
||||
|
||||
files = manifest.get("files", []) if files is None else files
|
||||
omitted = manifest.get("omitted_files", []) if omitted is None else omitted
|
||||
return kept(files), kept(omitted)
|
||||
|
||||
|
||||
def canonical_region(raw: str) -> str:
|
||||
"""One region name or alias, spelled the way manifests spell it."""
|
||||
key = raw.strip().lower()
|
||||
@@ -1393,7 +1591,11 @@ def available_choices(files: list[dict], omitted: list[dict]) -> dict[str, list[
|
||||
ordered = sorted(systems)
|
||||
if any(not entry_systems(f) for f in files):
|
||||
ordered.append(OTHER_SYSTEMS)
|
||||
cores = {c for f in files for c in (f.get("cores") or [])}
|
||||
cores = {
|
||||
c for f in files
|
||||
for copy in [f, *f.get("alternatives", [])]
|
||||
for c in (copy.get("cores") or [])
|
||||
}
|
||||
regions = {
|
||||
r for e in files + omitted for r in (e.get("regions") or [])
|
||||
if r != WORLD_REGION
|
||||
@@ -2124,16 +2326,24 @@ def _pick(title: str, rows: list[tuple[str, list[dict]]], ordered: bool = False)
|
||||
return [rows[number - 1][0] for number in chosen]
|
||||
|
||||
|
||||
INSTALLED_ONLY = "installed"
|
||||
|
||||
|
||||
def _prompt_custom_selection(
|
||||
plat: str, files: list[dict], omitted: list[dict]
|
||||
) -> tuple[list[str], list[str], list[str]] | None:
|
||||
plat: str, files: list[dict], omitted: list[dict],
|
||||
installed: tuple[list[dict], list[dict]] | None = None,
|
||||
) -> tuple[list[str], list[str], list[str]] | str | None:
|
||||
"""Offer to narrow the install by system, core and region.
|
||||
|
||||
Enter installs everything, the default most runs want. Returns None
|
||||
then, or the three choices; each empty list keeps everything on its axis.
|
||||
then, INSTALLED_ONLY for what `installed` (the files the cores found
|
||||
installed read) holds, or the three choices; each empty list keeps
|
||||
everything on its axis.
|
||||
"""
|
||||
print(f"\n{plat.capitalize()}: {_sized(files)}.")
|
||||
print(" Enter) install everything")
|
||||
if installed is not None:
|
||||
print(f" i) only what the installed cores read: {_sized(installed[0])}")
|
||||
print(" c) choose systems, cores and regions")
|
||||
print(" q) quit")
|
||||
while True:
|
||||
@@ -2142,6 +2352,8 @@ def _prompt_custom_selection(
|
||||
return None
|
||||
if answer == "q":
|
||||
sys.exit(0)
|
||||
if answer == "i" and installed is not None:
|
||||
return INSTALLED_ONLY
|
||||
if answer == "c":
|
||||
break
|
||||
|
||||
@@ -2161,7 +2373,8 @@ def _prompt_custom_selection(
|
||||
print("\nThe platform's own files for these systems are always kept;")
|
||||
print("the extra files each core loads can be narrowed.")
|
||||
cores = _pick("Cores:", [
|
||||
(name, [f for f in kept if name in (f.get("cores") or [])])
|
||||
(name, _filter_by_target(
|
||||
[f for f in kept if f.get("cores") is not None], [name]))
|
||||
for name in core_names
|
||||
])
|
||||
kept, kept_omitted = narrow(kept, kept_omitted, [], cores, [])
|
||||
@@ -2205,7 +2418,7 @@ def _print_choices(
|
||||
held = (
|
||||
[f for f in files if _chosen(f, {name})]
|
||||
if systems
|
||||
else [f for f in files if name in (f.get("cores") or [])]
|
||||
else _filter_by_target([f for f in files if f.get("cores") is not None], [name])
|
||||
)
|
||||
print(f" {name:<{width}} {_sized(held)}")
|
||||
|
||||
@@ -2285,6 +2498,12 @@ def main() -> None:
|
||||
action="store_true",
|
||||
help="opt in to copies into detected standalone-emulator directories",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--installed-cores",
|
||||
action="store_true",
|
||||
help="install only what the cores already installed read "
|
||||
"(RetroArch, Lakka, RetroPie)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--system",
|
||||
action="append",
|
||||
@@ -2453,6 +2672,28 @@ def _install(args: argparse.Namespace, real_stdout) -> None:
|
||||
failures: list[tuple[str, str]] = []
|
||||
installed_paths: list[Path] = []
|
||||
|
||||
# Every check --installed-cores makes before anything is written: a
|
||||
# refusal for the second platform must not follow an install of the first.
|
||||
cores_found: dict[tuple[str, Path], list[str]] = {}
|
||||
for plat_name, bios_path in platforms:
|
||||
if plat_name in CORE_PLATFORMS:
|
||||
searched = core_dirs(plat_name, bios_path)
|
||||
cores_found[(plat_name, bios_path)] = installed_cores(searched)
|
||||
elif args.installed_cores:
|
||||
print(
|
||||
f"Error: {plat_name} ships its cores with the system; "
|
||||
f"--installed-cores applies to {', '.join(CORE_PLATFORMS)}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if args.installed_cores and not cores_found[(plat_name, bios_path)]:
|
||||
print(
|
||||
f"Error: no installed core found for {plat_name} in: "
|
||||
f"{', '.join(map(str, searched)) or 'no known directory'}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
for plat_name, bios_path in platforms:
|
||||
if bios_path not in installed_paths:
|
||||
installed_paths.append(bios_path)
|
||||
@@ -2503,12 +2744,31 @@ def _install(args: argparse.Namespace, real_stdout) -> None:
|
||||
"installing every file"
|
||||
)
|
||||
else:
|
||||
target_cores = target_info["cores"]
|
||||
before = len(files)
|
||||
files = _filter_by_target(files, target_cores)
|
||||
omitted_files = _filter_by_target(omitted_files, target_cores)
|
||||
files, omitted_files = narrow_to_cores(
|
||||
manifest, target_info["cores"], files, omitted_files
|
||||
)
|
||||
print(f" Filtered {before} -> {len(files)} files for target {args.target}")
|
||||
|
||||
found_cores = cores_found.get((plat_name, bios_path), [])
|
||||
if "system_cores" not in manifest:
|
||||
found_cores = []
|
||||
elif found_cores:
|
||||
print(f" {_plural(len(found_cores), 'core')} installed")
|
||||
if args.installed_cores:
|
||||
if "system_cores" not in manifest:
|
||||
print(
|
||||
f"Error: the {plat_name} file list does not say which cores "
|
||||
"read which systems; --installed-cores needs a newer one",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
before = len(files)
|
||||
files, omitted_files = narrow_to_cores(
|
||||
manifest, found_cores, files, omitted_files
|
||||
)
|
||||
print(f" Narrowed {before} -> {len(files)} files to the installed cores")
|
||||
|
||||
selection = _requested_narrowing(args, plat_name, files, omitted_files)
|
||||
if (
|
||||
selection is None
|
||||
@@ -2517,8 +2777,19 @@ def _install(args: argparse.Namespace, real_stdout) -> None:
|
||||
and not listing
|
||||
and records_selection(files)
|
||||
):
|
||||
selection = _prompt_custom_selection(plat_name, files, omitted_files)
|
||||
if selection is not None:
|
||||
installed = (
|
||||
narrow_to_cores(manifest, found_cores, files, omitted_files)
|
||||
if found_cores and not args.installed_cores else None
|
||||
)
|
||||
selection = _prompt_custom_selection(
|
||||
plat_name, files, omitted_files, installed
|
||||
)
|
||||
if selection == INSTALLED_ONLY:
|
||||
files, omitted_files = installed
|
||||
print("\n Same choice without questions: "
|
||||
f"--platform {plat_name} --installed-cores")
|
||||
selection = None
|
||||
elif selection is not None:
|
||||
print("\n Same choice without questions: "
|
||||
+ _same_choice_hint(plat_name, *selection))
|
||||
if selection is not None:
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ esac
|
||||
TEMP_INSTALLER=""
|
||||
TEMP_DIRECTORY=""
|
||||
DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
|
||||
DEFAULT_INSTALL_SHA256="95e9cf9c274d3ccf5a305ee4a32e74c5c173177b7cf012069835a096e7ff2b35"
|
||||
DEFAULT_INSTALL_SHA256="0aa7b10a9c6f295366f5d22df012d7205af4ccf4cc9d9cdc9cd90872cc4d6bf3"
|
||||
MAX_INSTALLER_BYTES=2097152
|
||||
|
||||
cleanup() {
|
||||
|
||||
+1259
-422
File diff suppressed because it is too large.
Load diff
+748
-26
@@ -5,7 +5,7 @@
|
||||
"platform": "bizhawk",
|
||||
"display_name": "BizHawk",
|
||||
"version": "1.0",
|
||||
"generated": "2026-10-10T05:24:26Z",
|
||||
"generated": "2026-10-15T21:45:48Z",
|
||||
"base_destination": "Firmware",
|
||||
"detect": [
|
||||
{
|
||||
@@ -26,6 +26,620 @@
|
||||
"pack_size": 2585299183,
|
||||
"total_omitted": 0,
|
||||
"omitted_files": [],
|
||||
"core_names": {
|
||||
"3dsen": [
|
||||
"3dsen-windows"
|
||||
],
|
||||
"beetle_gba": [
|
||||
"mednafen_gba"
|
||||
],
|
||||
"beetle_lynx": [
|
||||
"mednafen_lynx"
|
||||
],
|
||||
"beetle_pce": [
|
||||
"mednafen_pce"
|
||||
],
|
||||
"beetle_pcfx": [
|
||||
"mednafen_pcfx",
|
||||
"pcfx"
|
||||
],
|
||||
"beetle_psx": [
|
||||
"mednafen_psx",
|
||||
"mednafen_psx_hw"
|
||||
],
|
||||
"beetle_saturn": [
|
||||
"beetle-saturn",
|
||||
"mednafen_saturn"
|
||||
],
|
||||
"bsnes2014": [
|
||||
"bsnes2014_accuracy",
|
||||
"bsnes2014_balanced",
|
||||
"bsnes2014_performance"
|
||||
],
|
||||
"bsnes_hd_beta": [
|
||||
"bsnes_hd",
|
||||
"bsneshd"
|
||||
],
|
||||
"bsnes_mercury": [
|
||||
"bsnes_mercury_accuracy",
|
||||
"bsnes_mercury_balanced",
|
||||
"bsnes_mercury_performance"
|
||||
],
|
||||
"c64-emu": [
|
||||
"C64-EMU",
|
||||
"C64.emu"
|
||||
],
|
||||
"doublecherrygb": [
|
||||
"DoubleCherryGB"
|
||||
],
|
||||
"ep128emu": [
|
||||
"ep128emu_core"
|
||||
],
|
||||
"ep128emu_core": [
|
||||
"ep128emu-core"
|
||||
],
|
||||
"fpse-ng": [
|
||||
"fpse64",
|
||||
"fpseng"
|
||||
],
|
||||
"gba-emu": [
|
||||
"GBA-EMU",
|
||||
"GBA.emu"
|
||||
],
|
||||
"gbc-emu": [
|
||||
"GBC-EMU",
|
||||
"GBC.emu"
|
||||
],
|
||||
"genesis_plus_gx": [
|
||||
"genesisplusgx",
|
||||
"genesisplusgx-expanded",
|
||||
"genplus"
|
||||
],
|
||||
"genesis_plus_gx_wide": [
|
||||
"genesisplusgx-wide",
|
||||
"genesisplusgxwide"
|
||||
],
|
||||
"higan_sfc_balanced": [
|
||||
"nside_sfc_balanced"
|
||||
],
|
||||
"idtech4a++": [
|
||||
"diii4a",
|
||||
"idtech4amm"
|
||||
],
|
||||
"lime3ds": [
|
||||
"lime-qt",
|
||||
"lime3ds-gui"
|
||||
],
|
||||
"linkboy": [
|
||||
"LINKBOY",
|
||||
"Linkboy"
|
||||
],
|
||||
"lynx-emu": [
|
||||
"LYNX-EMU",
|
||||
"Lynx.emu"
|
||||
],
|
||||
"m64plus-fz": [
|
||||
"M64PLUS-FZ",
|
||||
"org.mupen64plusae.v3.fzurita",
|
||||
"org.mupen64plusae.v3.fzurita.amazon",
|
||||
"org.mupen64plusae.v3.fzurita.pro"
|
||||
],
|
||||
"mame": [
|
||||
"imame4all"
|
||||
],
|
||||
"mame2016": [
|
||||
"mess2016"
|
||||
],
|
||||
"mame4droid-2024": [
|
||||
"MAME4DROID-2024",
|
||||
"MAME4DROID-CURRENT",
|
||||
"com.seleuco.mame4d2024"
|
||||
],
|
||||
"mandarine": [
|
||||
"mandarine-qt"
|
||||
],
|
||||
"mastergear": [
|
||||
"MASTERGEAR",
|
||||
"com.fms.mg"
|
||||
],
|
||||
"md-emu": [
|
||||
"MD-EMU",
|
||||
"MD.emu"
|
||||
],
|
||||
"mednafen_snes": [
|
||||
"beetle-bsnes",
|
||||
"beetle_bsnes"
|
||||
],
|
||||
"mednafen_supafaust": [
|
||||
"supafaust"
|
||||
],
|
||||
"melonds_ds": [
|
||||
"melondsds"
|
||||
],
|
||||
"mesen": [
|
||||
"mesens"
|
||||
],
|
||||
"mesen-s": [
|
||||
"mesen_s"
|
||||
],
|
||||
"mesence": [
|
||||
"mesen2"
|
||||
],
|
||||
"msx-emu": [
|
||||
"MSX-EMU",
|
||||
"MSX.emu"
|
||||
],
|
||||
"mupen64plus-ae": [
|
||||
"MUPEN64PLUS-AE",
|
||||
"org.mupen64plusae.v3.alpha"
|
||||
],
|
||||
"mupen64plus_next": [
|
||||
"mupen64plus",
|
||||
"mupen64plus-next",
|
||||
"mupen64plus_next_develop",
|
||||
"mupen64plus_next_gles2",
|
||||
"mupen64plus_next_gles3"
|
||||
],
|
||||
"my-boy": [
|
||||
"MY-BOY",
|
||||
"My Boy!"
|
||||
],
|
||||
"my-oldboy": [
|
||||
"MY-OLDBOY",
|
||||
"My OldBoy!"
|
||||
],
|
||||
"nes-emu": [
|
||||
"NES-EMU",
|
||||
"NES.emu"
|
||||
],
|
||||
"nesoid": [
|
||||
"NESOID",
|
||||
"Nesoid"
|
||||
],
|
||||
"o2em": [
|
||||
"o2em_libretro"
|
||||
],
|
||||
"parallel-launcher": [
|
||||
"ca.parallel_launcher.ParallelLauncher"
|
||||
],
|
||||
"parallel_n64": [
|
||||
"parallel_n64_debug"
|
||||
],
|
||||
"pce-emu": [
|
||||
"PCE-EMU",
|
||||
"PCE.emu",
|
||||
"com.PceEmu",
|
||||
"pceemu"
|
||||
],
|
||||
"pcsx_rearmed": [
|
||||
"pcsx_rearmed_interpreter",
|
||||
"pcsx_rearmed_neon"
|
||||
],
|
||||
"pizza-boy-gba": [
|
||||
"PIZZA-BOY-GBA",
|
||||
"Pizza Boy GBA"
|
||||
],
|
||||
"pizza-boy-gbc": [
|
||||
"PIZZA-BOY-GBC",
|
||||
"Pizza Boy GBC"
|
||||
],
|
||||
"pizza-boy-sc": [
|
||||
"PIZZA-BOY-SC",
|
||||
"Pizza Boy SC",
|
||||
"it.dbtecno.pizzaboyscbasic",
|
||||
"it.dbtecno.pizzaboyscpro"
|
||||
],
|
||||
"puae": [
|
||||
"uae"
|
||||
],
|
||||
"real3doplayer": [
|
||||
"REAL3DOPLAYER",
|
||||
"Real3DOPlayer"
|
||||
],
|
||||
"rosalies-mupen-gui": [
|
||||
"RMG",
|
||||
"com.github.Rosalie241.RMG"
|
||||
],
|
||||
"saturn-emu": [
|
||||
"SATURN-EMU",
|
||||
"Saturn.emu",
|
||||
"saturnemu"
|
||||
],
|
||||
"smsplus": [
|
||||
"smsplus_gx"
|
||||
],
|
||||
"snes9x": [
|
||||
"pocketsnes",
|
||||
"snes9x_next"
|
||||
],
|
||||
"snes9x-explus": [
|
||||
"SNES9X-EXPLUS",
|
||||
"Snes9x EX+",
|
||||
"Snes9xEXPlus"
|
||||
],
|
||||
"speccy": [
|
||||
"SPECCY",
|
||||
"com.fms.speccy",
|
||||
"com.fms.speccy.deluxe"
|
||||
],
|
||||
"stella2023": [
|
||||
"stella"
|
||||
],
|
||||
"superzsnes": [
|
||||
"SUPERZSNES"
|
||||
],
|
||||
"tgbdual": [
|
||||
"TGB Dual"
|
||||
],
|
||||
"vbam": [
|
||||
"vba-m"
|
||||
],
|
||||
"vice": [
|
||||
"vice_x128",
|
||||
"vice_x64",
|
||||
"vice_x64dtv",
|
||||
"vice_x64sc",
|
||||
"vice_xcbm2",
|
||||
"vice_xcbm5x0",
|
||||
"vice_xpet",
|
||||
"vice_xplus4",
|
||||
"vice_xscpu64",
|
||||
"vice_xvic"
|
||||
],
|
||||
"vice_x64": [
|
||||
"x64"
|
||||
],
|
||||
"yabasanshiro": [
|
||||
"yabasanshiro-2"
|
||||
]
|
||||
},
|
||||
"system_cores": {
|
||||
"3do": [
|
||||
"groovymame",
|
||||
"mame",
|
||||
"mame2015",
|
||||
"mame2016",
|
||||
"mame4droid-2024",
|
||||
"opera",
|
||||
"real3doplayer",
|
||||
"ume2015"
|
||||
],
|
||||
"apple-ii": [
|
||||
"applewin",
|
||||
"clk",
|
||||
"izapple2",
|
||||
"kegs",
|
||||
"linapple",
|
||||
"mariani",
|
||||
"mednafen",
|
||||
"pom2"
|
||||
],
|
||||
"atari-7800": [
|
||||
"a7800",
|
||||
"prosystem"
|
||||
],
|
||||
"atari-lynx": [
|
||||
"beetle_lynx",
|
||||
"gearlynx",
|
||||
"handy",
|
||||
"holani",
|
||||
"lynx-emu",
|
||||
"mednafen"
|
||||
],
|
||||
"coleco-colecovision": [
|
||||
"ares",
|
||||
"bluemsx",
|
||||
"clk",
|
||||
"colem",
|
||||
"fbneo",
|
||||
"gearcoleco",
|
||||
"jollycv",
|
||||
"mesence",
|
||||
"msx-emu",
|
||||
"openmsx",
|
||||
"smsplus",
|
||||
"zesarux"
|
||||
],
|
||||
"commodore-amiga": [
|
||||
"amiberry",
|
||||
"clk",
|
||||
"fsuae",
|
||||
"puae",
|
||||
"puae2021",
|
||||
"uae4arm"
|
||||
],
|
||||
"commodore-c64": [
|
||||
"c64-emu",
|
||||
"frodo",
|
||||
"vice",
|
||||
"vice_x64",
|
||||
"vice_x64sc",
|
||||
"x64sdl"
|
||||
],
|
||||
"doom": [
|
||||
"gzdoom",
|
||||
"idtech4a++",
|
||||
"prboom",
|
||||
"prboom-plus"
|
||||
],
|
||||
"fairchild-channel-f": [
|
||||
"fbneo",
|
||||
"freechaf"
|
||||
],
|
||||
"gce-vectrex": [
|
||||
"vecx"
|
||||
],
|
||||
"mattel-intellivision": [
|
||||
"freeintv"
|
||||
],
|
||||
"microsoft-msx": [
|
||||
"ares",
|
||||
"bluemsx",
|
||||
"clk",
|
||||
"fbneo",
|
||||
"fmsx",
|
||||
"msx-emu",
|
||||
"openmsx",
|
||||
"zesarux"
|
||||
],
|
||||
"nec-pc-engine-cd": [
|
||||
"ares",
|
||||
"geargrafx",
|
||||
"mednafen",
|
||||
"mesence",
|
||||
"pce-emu"
|
||||
],
|
||||
"nec-pc-fx": [
|
||||
"beetle_pcfx",
|
||||
"mednafen"
|
||||
],
|
||||
"nintendo-3ds": [
|
||||
"azahar",
|
||||
"azaharplus",
|
||||
"citra",
|
||||
"citra-mmj",
|
||||
"citra2018",
|
||||
"citra_canary",
|
||||
"lime3ds",
|
||||
"mandarine",
|
||||
"panda3ds",
|
||||
"trident"
|
||||
],
|
||||
"nintendo-64dd": [
|
||||
"ares",
|
||||
"m64plus-fz",
|
||||
"mupen64plus-ae",
|
||||
"mupen64plus_next",
|
||||
"parallel-launcher",
|
||||
"parallel_n64",
|
||||
"project64",
|
||||
"rosalies-mupen-gui",
|
||||
"sixtyforce"
|
||||
],
|
||||
"nintendo-ds": [
|
||||
"desmume",
|
||||
"desmume2015",
|
||||
"drastic",
|
||||
"melonds",
|
||||
"melonds-nightly",
|
||||
"melonds_ds",
|
||||
"noods",
|
||||
"skyemu"
|
||||
],
|
||||
"nintendo-gb": [
|
||||
"boytacean",
|
||||
"doublecherrygb",
|
||||
"emux_gb",
|
||||
"gambatte",
|
||||
"gbc-emu",
|
||||
"gearboy",
|
||||
"irogb",
|
||||
"jgenesis",
|
||||
"linkboy",
|
||||
"mednafen",
|
||||
"mesen-s",
|
||||
"mesence",
|
||||
"mgba",
|
||||
"my-oldboy",
|
||||
"pizza-boy-gbc",
|
||||
"sameboy",
|
||||
"skyemu",
|
||||
"tgbdual",
|
||||
"vbam"
|
||||
],
|
||||
"nintendo-gba": [
|
||||
"beetle_gba",
|
||||
"gba-emu",
|
||||
"gpsp",
|
||||
"jgenesis",
|
||||
"linkboy",
|
||||
"mednafen",
|
||||
"mednafen_gba",
|
||||
"mesence",
|
||||
"meteor",
|
||||
"mgba",
|
||||
"mgba_splitscreen",
|
||||
"my-boy",
|
||||
"noods",
|
||||
"pizza-boy-gba",
|
||||
"skyemu",
|
||||
"tempgba",
|
||||
"vba_next",
|
||||
"vbam"
|
||||
],
|
||||
"nintendo-gbc": [
|
||||
"boytacean",
|
||||
"doublecherrygb",
|
||||
"fixgb",
|
||||
"gambatte",
|
||||
"gbc-emu",
|
||||
"gearboy",
|
||||
"irogb",
|
||||
"jgenesis",
|
||||
"linkboy",
|
||||
"mednafen",
|
||||
"mesen-s",
|
||||
"mesence",
|
||||
"mgba",
|
||||
"my-oldboy",
|
||||
"pizza-boy-gbc",
|
||||
"sameboy",
|
||||
"skyemu",
|
||||
"tgbdual",
|
||||
"vbam"
|
||||
],
|
||||
"nintendo-nes": [
|
||||
"3dsen",
|
||||
"bnes",
|
||||
"emux_nes",
|
||||
"fceumm",
|
||||
"fixnes",
|
||||
"ines",
|
||||
"jgenesis",
|
||||
"mednafen",
|
||||
"mesen",
|
||||
"mesence",
|
||||
"nes",
|
||||
"nes-emu",
|
||||
"nesoid",
|
||||
"nestopia",
|
||||
"plastic",
|
||||
"punes",
|
||||
"quicknes",
|
||||
"rustynes"
|
||||
],
|
||||
"nintendo-satellaview": [
|
||||
"bsnes",
|
||||
"bsnes-jg",
|
||||
"bsnes2014",
|
||||
"bsnes_cplusplus98",
|
||||
"bsnes_hd_beta",
|
||||
"bsnes_mercury",
|
||||
"chimerasnes",
|
||||
"mesen-s",
|
||||
"mesence",
|
||||
"snes9x",
|
||||
"snes9x-explus",
|
||||
"snes9x2002",
|
||||
"snes9x2005",
|
||||
"snes9x2005_plus",
|
||||
"snes9x2010"
|
||||
],
|
||||
"nintendo-snes": [
|
||||
"bsnes",
|
||||
"bsnes-jg",
|
||||
"bsnes2014",
|
||||
"bsnes_cplusplus98",
|
||||
"bsnes_hd_beta",
|
||||
"bsnes_mercury",
|
||||
"chimerasnes",
|
||||
"fbneo",
|
||||
"higan_sfc",
|
||||
"higan_sfc_balanced",
|
||||
"jgenesis",
|
||||
"mednafen",
|
||||
"mednafen_snes",
|
||||
"mednafen_supafaust",
|
||||
"mesen-s",
|
||||
"mesence",
|
||||
"snes9x",
|
||||
"snes9x-explus",
|
||||
"snes9x2002",
|
||||
"snes9x2005",
|
||||
"snes9x2005_plus",
|
||||
"snes9x2010",
|
||||
"superzsnes"
|
||||
],
|
||||
"philips-videopac": [
|
||||
"o2em"
|
||||
],
|
||||
"sega-32x": [
|
||||
"ares",
|
||||
"jgenesis",
|
||||
"picodrive"
|
||||
],
|
||||
"sega-game-gear": [
|
||||
"ares",
|
||||
"gearsystem",
|
||||
"genesis_plus_gx",
|
||||
"genesis_plus_gx_wide",
|
||||
"jgenesis",
|
||||
"mastergear",
|
||||
"mednafen",
|
||||
"mesence",
|
||||
"picodrive",
|
||||
"pizza-boy-sc",
|
||||
"smsplus"
|
||||
],
|
||||
"sega-master-system": [
|
||||
"ares",
|
||||
"blastem",
|
||||
"clk",
|
||||
"gearsystem",
|
||||
"genesis_plus_gx",
|
||||
"genesis_plus_gx_wide",
|
||||
"jgenesis",
|
||||
"mastergear",
|
||||
"md-emu",
|
||||
"mednafen",
|
||||
"mesence",
|
||||
"picodrive",
|
||||
"pizza-boy-sc",
|
||||
"smsplus",
|
||||
"zesarux"
|
||||
],
|
||||
"sega-mega-drive": [
|
||||
"ares",
|
||||
"blastem",
|
||||
"clownmdemu",
|
||||
"genesis_plus_gx",
|
||||
"genesis_plus_gx_wide",
|
||||
"jgenesis",
|
||||
"md-emu",
|
||||
"mednafen",
|
||||
"picodrive",
|
||||
"pizza-boy-sc"
|
||||
],
|
||||
"sega-saturn": [
|
||||
"ares",
|
||||
"beetle_saturn",
|
||||
"kronos",
|
||||
"mednafen",
|
||||
"saturn-emu",
|
||||
"ssf",
|
||||
"yabasanshiro",
|
||||
"yabause",
|
||||
"ymir"
|
||||
],
|
||||
"sinclair-zx-spectrum": [
|
||||
"81",
|
||||
"ares",
|
||||
"clk",
|
||||
"cspect",
|
||||
"ep128emu",
|
||||
"ep128emu_core",
|
||||
"fuse",
|
||||
"speccy",
|
||||
"zesarux"
|
||||
],
|
||||
"sony-playstation": [
|
||||
"ares",
|
||||
"armsx1",
|
||||
"beetle_psx",
|
||||
"duckstation",
|
||||
"epsxe",
|
||||
"fpse",
|
||||
"fpse-ng",
|
||||
"mednafen",
|
||||
"pcsx1",
|
||||
"pcsx_rearmed",
|
||||
"rustation",
|
||||
"swanstation"
|
||||
],
|
||||
"texas-instruments-ti-83": [
|
||||
"numero"
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
{
|
||||
"dest": "panafz1.bin",
|
||||
@@ -1379,7 +1993,8 @@
|
||||
"size": 16384,
|
||||
"repo_path": "bios/Nintendo/Game Boy Advance/GBA_bios.rom",
|
||||
"cores": [
|
||||
"ares"
|
||||
"ares",
|
||||
"mgba"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1389,7 +2004,8 @@
|
||||
"size": 1024,
|
||||
"repo_path": "bios/Sega/Game Gear/GG_majesco.gg",
|
||||
"cores": [
|
||||
"ares"
|
||||
"ares",
|
||||
"genesis_plus_gx"
|
||||
],
|
||||
"systems": [
|
||||
"sega-game-gear"
|
||||
@@ -1402,7 +2018,8 @@
|
||||
"size": 8192,
|
||||
"repo_path": "bios/Other/zesarux/sms.rom",
|
||||
"cores": [
|
||||
"ares"
|
||||
"ares",
|
||||
"genesis_plus_gx"
|
||||
],
|
||||
"systems": [
|
||||
"sega-master-system"
|
||||
@@ -1415,7 +2032,8 @@
|
||||
"size": 8192,
|
||||
"repo_path": "bios/Sega/Master System/MasterSystem/japanese-bios.sms",
|
||||
"cores": [
|
||||
"ares"
|
||||
"ares",
|
||||
"genesis_plus_gx"
|
||||
],
|
||||
"systems": [
|
||||
"sega-master-system"
|
||||
@@ -1428,7 +2046,8 @@
|
||||
"size": 8192,
|
||||
"repo_path": "bios/Other/zesarux/sms.rom",
|
||||
"cores": [
|
||||
"ares"
|
||||
"ares",
|
||||
"genesis_plus_gx"
|
||||
],
|
||||
"systems": [
|
||||
"sega-master-system"
|
||||
@@ -1441,7 +2060,9 @@
|
||||
"size": 131072,
|
||||
"repo_path": "bios/Sega/Mega CD/SegaCDBIOS9303.bin",
|
||||
"cores": [
|
||||
"ares"
|
||||
"ares",
|
||||
"genesis_plus_gx",
|
||||
"picodrive"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1451,7 +2072,9 @@
|
||||
"size": 131072,
|
||||
"repo_path": "bios/Sega/Mega CD/jp_mcd2_921222.bin",
|
||||
"cores": [
|
||||
"ares"
|
||||
"ares",
|
||||
"genesis_plus_gx",
|
||||
"picodrive"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1461,7 +2084,9 @@
|
||||
"size": 131072,
|
||||
"repo_path": "bios/Sega/Mega CD/eu_mcd2_9303.bin",
|
||||
"cores": [
|
||||
"ares"
|
||||
"ares",
|
||||
"genesis_plus_gx",
|
||||
"picodrive"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1601,7 +2226,8 @@
|
||||
"size": 262144,
|
||||
"repo_path": "bios/NEC/PC Engine/syscard1.pce",
|
||||
"cores": [
|
||||
"ares"
|
||||
"ares",
|
||||
"beetle_pce"
|
||||
],
|
||||
"systems": [
|
||||
"nec-pc-engine-cd"
|
||||
@@ -1614,7 +2240,8 @@
|
||||
"size": 262144,
|
||||
"repo_path": "bios/NEC/PC Engine/syscard3.pce",
|
||||
"cores": [
|
||||
"ares"
|
||||
"ares",
|
||||
"beetle_pce"
|
||||
],
|
||||
"systems": [
|
||||
"nec-pc-engine-cd"
|
||||
@@ -1627,7 +2254,8 @@
|
||||
"size": 262144,
|
||||
"repo_path": "bios/NEC/PC Engine/syscard3u.pce",
|
||||
"cores": [
|
||||
"ares"
|
||||
"ares",
|
||||
"beetle_pce"
|
||||
],
|
||||
"systems": [
|
||||
"nec-pc-engine-cd"
|
||||
@@ -1683,7 +2311,8 @@
|
||||
"size": 524288,
|
||||
"repo_path": "bios/Sony/PlayStation/scph5501.bin",
|
||||
"cores": [
|
||||
"ares"
|
||||
"ares",
|
||||
"beetle_psx"
|
||||
],
|
||||
"systems": [
|
||||
"sony-playstation"
|
||||
@@ -1696,7 +2325,8 @@
|
||||
"size": 524288,
|
||||
"repo_path": "bios/Sony/PlayStation/scph5500.bin",
|
||||
"cores": [
|
||||
"ares"
|
||||
"ares",
|
||||
"beetle_psx"
|
||||
],
|
||||
"systems": [
|
||||
"sony-playstation"
|
||||
@@ -1709,7 +2339,8 @@
|
||||
"size": 524288,
|
||||
"repo_path": "bios/Sony/PlayStation/scph5502.bin",
|
||||
"cores": [
|
||||
"ares"
|
||||
"ares",
|
||||
"beetle_psx"
|
||||
],
|
||||
"systems": [
|
||||
"sony-playstation"
|
||||
@@ -1779,7 +2410,8 @@
|
||||
"size": 262144,
|
||||
"repo_path": "bios/Nintendo/Super Game Boy/SGB1.sfc/program.rom",
|
||||
"cores": [
|
||||
"ares"
|
||||
"ares",
|
||||
"bsnes"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1789,7 +2421,8 @@
|
||||
"size": 524288,
|
||||
"repo_path": "bios/Nintendo/Super Game Boy/SGB2.sfc/program.rom",
|
||||
"cores": [
|
||||
"ares"
|
||||
"ares",
|
||||
"bsnes"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1799,7 +2432,8 @@
|
||||
"size": 262144,
|
||||
"repo_path": "bios/Nintendo/SuFami Turbo/STBIOS.bin",
|
||||
"cores": [
|
||||
"ares"
|
||||
"ares",
|
||||
"snes9x"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1809,7 +2443,9 @@
|
||||
"size": 1048576,
|
||||
"repo_path": "bios/Nintendo/SNES/BS-X.bin",
|
||||
"cores": [
|
||||
"ares"
|
||||
"ares",
|
||||
"bsnes",
|
||||
"snes9x"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -2099,6 +2735,17 @@
|
||||
"repo_path": "bios/Arcade/Arcade/stvbios.zip",
|
||||
"cores": [
|
||||
"beetle_saturn"
|
||||
],
|
||||
"alternatives": [
|
||||
{
|
||||
"sha1": "7783ab663bb1a8d62ff60d583674355c2af556b1",
|
||||
"sha256": "38ceb2792c72770f56aadb1865cf66dc33a3017db1ee05e11c2b2dff397ad39e",
|
||||
"size": 2955630,
|
||||
"repo_path": "bios/Arcade/MAME/stvbios.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -2368,7 +3015,9 @@
|
||||
"size": 3145728,
|
||||
"repo_path": "bios/Nintendo/3DS/Citra/sysdata/shared_font.bin",
|
||||
"cores": [
|
||||
"citra"
|
||||
"citra",
|
||||
"citra2018",
|
||||
"citra_canary"
|
||||
],
|
||||
"systems": [
|
||||
"nintendo-3ds"
|
||||
@@ -3989,7 +4638,8 @@
|
||||
"size": 256,
|
||||
"repo_path": "bios/Nintendo/Game Boy/GB_dmg.bin",
|
||||
"cores": [
|
||||
"gambatte"
|
||||
"gambatte",
|
||||
"mgba"
|
||||
],
|
||||
"systems": [
|
||||
"nintendo-gb"
|
||||
@@ -4002,7 +4652,8 @@
|
||||
"size": 2304,
|
||||
"repo_path": "bios/Nintendo/Game Boy Color/GBC_cgb.bin",
|
||||
"cores": [
|
||||
"gambatte"
|
||||
"gambatte",
|
||||
"mgba"
|
||||
],
|
||||
"systems": [
|
||||
"nintendo-gbc"
|
||||
@@ -4107,6 +4758,17 @@
|
||||
"repo_path": "bios/Arcade/MAME/neogeo.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
],
|
||||
"alternatives": [
|
||||
{
|
||||
"sha1": "deb62b0074b8cae4f162c257662136733cfc76ad",
|
||||
"sha256": "bf05cc848e403f8ae457bfe26faa8135cf4725cfff956fa030f5c0a00a26adcc",
|
||||
"size": 1859335,
|
||||
"repo_path": "bios/Arcade/Arcade/neogeo.zip",
|
||||
"cores": [
|
||||
"fbneo"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -4116,7 +4778,8 @@
|
||||
"size": 2094636,
|
||||
"repo_path": "bios/Arcade/MAME/pgm.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
"mame",
|
||||
"fbneo"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -4321,6 +4984,17 @@
|
||||
],
|
||||
"systems": [
|
||||
"3do"
|
||||
],
|
||||
"alternatives": [
|
||||
{
|
||||
"sha1": "f590ccf688b4c05fa1da5c5dd92c224545170c3b",
|
||||
"sha256": "c481bfde0ae68e5fbe70a406af1698fcba5b76d7e23c860c2715cacf019efad4",
|
||||
"size": 10207,
|
||||
"repo_path": "bios/Arcade/Arcade/isgsm.zip",
|
||||
"cores": [
|
||||
"fbneo"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -4344,6 +5018,17 @@
|
||||
"repo_path": "bios/Arcade/MAME/decocass.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
],
|
||||
"alternatives": [
|
||||
{
|
||||
"sha1": "1b33cf0e730a6cde96479b0face108b6931d433e",
|
||||
"sha256": "bd86d293abab2e816d307626cdd793e2da2ed69c4124bf92f7e7eca69f9a5096",
|
||||
"size": 22379,
|
||||
"repo_path": "bios/Arcade/Arcade/decocass.zip",
|
||||
"cores": [
|
||||
"fbneo"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -4492,7 +5177,8 @@
|
||||
"size": 7950,
|
||||
"repo_path": "bios/Arcade/Arcade/bubsys.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
"mame",
|
||||
"fbneo"
|
||||
],
|
||||
"systems": [
|
||||
"3do"
|
||||
@@ -4695,6 +5381,17 @@
|
||||
"repo_path": "bios/Arcade/MAME/skns.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
],
|
||||
"alternatives": [
|
||||
{
|
||||
"sha1": "4257bd14b541fafbd555cb98ba079a3416a45934",
|
||||
"sha256": "6edf22c68e70be06c9b32741e0d1df1aa38cf6fc162c1027c15d6c1d882f621b",
|
||||
"size": 924762,
|
||||
"repo_path": "bios/Arcade/Arcade/skns.zip",
|
||||
"cores": [
|
||||
"fbneo"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -4796,6 +5493,17 @@
|
||||
],
|
||||
"systems": [
|
||||
"3do"
|
||||
],
|
||||
"alternatives": [
|
||||
{
|
||||
"sha1": "4fb1f00681d2540b476add2def2bbf07ca3e3993",
|
||||
"sha256": "1cf61e6b54589ed9bf2b5f319c2b56de205384d6726b7a0fae79213071f3cfb5",
|
||||
"size": 49385,
|
||||
"repo_path": "bios/Arcade/Arcade/atarisy1.zip",
|
||||
"cores": [
|
||||
"fbneo"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -5052,7 +5760,8 @@
|
||||
"size": 6800,
|
||||
"repo_path": "bios/Bally/Astrocade/astrocde.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
"mame",
|
||||
"fbneo"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -5672,6 +6381,17 @@
|
||||
],
|
||||
"systems": [
|
||||
"3do"
|
||||
],
|
||||
"alternatives": [
|
||||
{
|
||||
"sha1": "06fc753d015b43ca1787f4cfd9331b1674202e64",
|
||||
"sha256": "187ede0f57d1439092a7ac50ea923533871ab7ee65e29baabc4dcdfa7ad4823f",
|
||||
"size": 7609,
|
||||
"repo_path": "bios/Arcade/Arcade/ym2608.zip",
|
||||
"cores": [
|
||||
"fbneo"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -10869,7 +11589,9 @@
|
||||
"size": 7357,
|
||||
"repo_path": "bios/Nintendo/3DS/aes_keys.txt",
|
||||
"cores": [
|
||||
"citra"
|
||||
"citra",
|
||||
"citra2018",
|
||||
"citra_canary"
|
||||
],
|
||||
"systems": [
|
||||
"nintendo-3ds"
|
||||
|
||||
+374
-23
@@ -5,7 +5,7 @@
|
||||
"platform": "emudeck",
|
||||
"display_name": "EmuDeck",
|
||||
"version": "1.0",
|
||||
"generated": "2026-10-10T05:24:26Z",
|
||||
"generated": "2026-10-15T21:45:48Z",
|
||||
"base_destination": "bios",
|
||||
"detect": [
|
||||
{
|
||||
@@ -256,6 +256,324 @@
|
||||
"pack_size": 3472458179,
|
||||
"total_omitted": 0,
|
||||
"omitted_files": [],
|
||||
"core_names": {
|
||||
"aethersx2": [
|
||||
"aethersx2-sa"
|
||||
],
|
||||
"armsx2": [
|
||||
"armsx2_libretro"
|
||||
],
|
||||
"beetle_psx": [
|
||||
"mednafen_psx",
|
||||
"mednafen_psx_hw"
|
||||
],
|
||||
"beetle_saturn": [
|
||||
"beetle-saturn",
|
||||
"mednafen_saturn"
|
||||
],
|
||||
"cxbx-reloaded": [
|
||||
"cxbx"
|
||||
],
|
||||
"fbalpha2012": [
|
||||
"fbalpha"
|
||||
],
|
||||
"flycast": [
|
||||
"flycast_dev",
|
||||
"flycastvl"
|
||||
],
|
||||
"fpse-ng": [
|
||||
"fpse64",
|
||||
"fpseng"
|
||||
],
|
||||
"genesis_plus_gx": [
|
||||
"genesisplusgx",
|
||||
"genesisplusgx-expanded",
|
||||
"genplus"
|
||||
],
|
||||
"genesis_plus_gx_wide": [
|
||||
"genesisplusgx-wide",
|
||||
"genesisplusgxwide"
|
||||
],
|
||||
"kenji-nx": [
|
||||
"KENJI-NX",
|
||||
"kenjinx"
|
||||
],
|
||||
"lime3ds": [
|
||||
"lime-qt",
|
||||
"lime3ds-gui"
|
||||
],
|
||||
"lrps2": [
|
||||
"pcsx2"
|
||||
],
|
||||
"m64plus-fz": [
|
||||
"M64PLUS-FZ",
|
||||
"org.mupen64plusae.v3.fzurita",
|
||||
"org.mupen64plusae.v3.fzurita.amazon",
|
||||
"org.mupen64plusae.v3.fzurita.pro"
|
||||
],
|
||||
"mame": [
|
||||
"imame4all"
|
||||
],
|
||||
"mame2016": [
|
||||
"mess2016"
|
||||
],
|
||||
"mame4droid-2024": [
|
||||
"MAME4DROID-2024",
|
||||
"MAME4DROID-CURRENT",
|
||||
"com.seleuco.mame4d2024"
|
||||
],
|
||||
"mandarine": [
|
||||
"mandarine-qt"
|
||||
],
|
||||
"md-emu": [
|
||||
"MD-EMU",
|
||||
"MD.emu"
|
||||
],
|
||||
"melonds_ds": [
|
||||
"melondsds"
|
||||
],
|
||||
"mesence": [
|
||||
"mesen2"
|
||||
],
|
||||
"msx-emu": [
|
||||
"MSX-EMU",
|
||||
"MSX.emu"
|
||||
],
|
||||
"mupen64plus-ae": [
|
||||
"MUPEN64PLUS-AE",
|
||||
"org.mupen64plusae.v3.alpha"
|
||||
],
|
||||
"mupen64plus_next": [
|
||||
"mupen64plus",
|
||||
"mupen64plus-next",
|
||||
"mupen64plus_next_develop",
|
||||
"mupen64plus_next_gles2",
|
||||
"mupen64plus_next_gles3"
|
||||
],
|
||||
"nethersx2": [
|
||||
"NETHERSX2",
|
||||
"NetherSX2"
|
||||
],
|
||||
"nethersx2-turnip": [
|
||||
"NETHERSX2-TURNIP",
|
||||
"NetherSX2-Turnip"
|
||||
],
|
||||
"nethersx2-turnip-classic": [
|
||||
"NETHERSX2-TURNIP-CLASSIC",
|
||||
"NetherSX2-Turnip-Classic"
|
||||
],
|
||||
"parallel-launcher": [
|
||||
"ca.parallel_launcher.ParallelLauncher"
|
||||
],
|
||||
"parallel_n64": [
|
||||
"parallel_n64_debug"
|
||||
],
|
||||
"pcsx2-legacy": [
|
||||
"PCSX2-LEGACY"
|
||||
],
|
||||
"pcsx_rearmed": [
|
||||
"pcsx_rearmed_interpreter",
|
||||
"pcsx_rearmed_neon"
|
||||
],
|
||||
"pizza-boy-sc": [
|
||||
"PIZZA-BOY-SC",
|
||||
"Pizza Boy SC",
|
||||
"it.dbtecno.pizzaboyscbasic",
|
||||
"it.dbtecno.pizzaboyscpro"
|
||||
],
|
||||
"play": [
|
||||
"play!"
|
||||
],
|
||||
"real3doplayer": [
|
||||
"REAL3DOPLAYER",
|
||||
"Real3DOPlayer"
|
||||
],
|
||||
"rosalies-mupen-gui": [
|
||||
"RMG",
|
||||
"com.github.Rosalie241.RMG"
|
||||
],
|
||||
"saturn-emu": [
|
||||
"SATURN-EMU",
|
||||
"Saturn.emu",
|
||||
"saturnemu"
|
||||
],
|
||||
"shadps4": [
|
||||
"shadps4-gui"
|
||||
],
|
||||
"simple64": [
|
||||
"io.github.simple64.simple64",
|
||||
"simple64-gui"
|
||||
],
|
||||
"skyline": [
|
||||
"SKYLINE"
|
||||
],
|
||||
"smsplus": [
|
||||
"smsplus_gx"
|
||||
],
|
||||
"triforce": [
|
||||
"dolphin-triforce"
|
||||
],
|
||||
"x1-box": [
|
||||
"x1box"
|
||||
],
|
||||
"yabasanshiro": [
|
||||
"yabasanshiro-2"
|
||||
]
|
||||
},
|
||||
"system_cores": {
|
||||
"coleco-colecovision": [
|
||||
"ares",
|
||||
"bluemsx",
|
||||
"clk",
|
||||
"colem",
|
||||
"fbneo",
|
||||
"gearcoleco",
|
||||
"jollycv",
|
||||
"mesence",
|
||||
"msx-emu",
|
||||
"openmsx",
|
||||
"smsplus",
|
||||
"zesarux"
|
||||
],
|
||||
"nintendo-3ds": [
|
||||
"azahar",
|
||||
"azaharplus",
|
||||
"citra",
|
||||
"citra-mmj",
|
||||
"citra2018",
|
||||
"citra_canary",
|
||||
"lime3ds",
|
||||
"mandarine",
|
||||
"panda3ds",
|
||||
"trident"
|
||||
],
|
||||
"nintendo-64": [
|
||||
"ares",
|
||||
"gopher64",
|
||||
"m64plus-fz",
|
||||
"mupen64plus-ae",
|
||||
"mupen64plus_next",
|
||||
"parallel-launcher",
|
||||
"parallel_n64",
|
||||
"project64",
|
||||
"rosalies-mupen-gui",
|
||||
"simple64",
|
||||
"sixtyforce"
|
||||
],
|
||||
"nintendo-ds": [
|
||||
"desmume",
|
||||
"desmume2015",
|
||||
"drastic",
|
||||
"melonds",
|
||||
"melonds-nightly",
|
||||
"melonds_ds",
|
||||
"noods",
|
||||
"skyemu"
|
||||
],
|
||||
"nintendo-gamecube": [
|
||||
"dolphin",
|
||||
"dolphin-mmjr",
|
||||
"dolphin-mmjr2",
|
||||
"dolphin_launcher",
|
||||
"ishiiruka",
|
||||
"primehack",
|
||||
"triforce"
|
||||
],
|
||||
"nintendo-switch": [
|
||||
"citron",
|
||||
"eden",
|
||||
"kenji-nx",
|
||||
"ryujinx",
|
||||
"skyline",
|
||||
"suyu",
|
||||
"yuzu"
|
||||
],
|
||||
"nintendo-wii-u": [
|
||||
"cemu"
|
||||
],
|
||||
"panasonic-3do": [
|
||||
"groovymame",
|
||||
"mame",
|
||||
"mame2015",
|
||||
"mame2016",
|
||||
"mame4droid-2024",
|
||||
"opera",
|
||||
"real3doplayer",
|
||||
"ume2015"
|
||||
],
|
||||
"sega-dreamcast": [
|
||||
"demul",
|
||||
"flycast",
|
||||
"flycast-dojo",
|
||||
"redream",
|
||||
"retrodream"
|
||||
],
|
||||
"sega-mega-cd": [
|
||||
"ares",
|
||||
"clownmdemu",
|
||||
"genesis_plus_gx",
|
||||
"genesis_plus_gx_wide",
|
||||
"jgenesis",
|
||||
"md-emu",
|
||||
"mednafen",
|
||||
"picodrive",
|
||||
"pizza-boy-sc"
|
||||
],
|
||||
"sega-saturn": [
|
||||
"ares",
|
||||
"beetle_saturn",
|
||||
"kronos",
|
||||
"mednafen",
|
||||
"saturn-emu",
|
||||
"ssf",
|
||||
"yabasanshiro",
|
||||
"yabause",
|
||||
"ymir"
|
||||
],
|
||||
"snk-neogeo": [
|
||||
"ares",
|
||||
"fbalpha2012",
|
||||
"fbalpha2012_neogeo"
|
||||
],
|
||||
"sony-playstation": [
|
||||
"ares",
|
||||
"armsx1",
|
||||
"beetle_psx",
|
||||
"duckstation",
|
||||
"epsxe",
|
||||
"fpse",
|
||||
"fpse-ng",
|
||||
"mednafen",
|
||||
"pcsx1",
|
||||
"pcsx_rearmed",
|
||||
"rustation",
|
||||
"swanstation"
|
||||
],
|
||||
"sony-playstation-2": [
|
||||
"aethersx2",
|
||||
"armsx2",
|
||||
"emucorex",
|
||||
"lrps2",
|
||||
"nethersx2",
|
||||
"nethersx2-turnip",
|
||||
"nethersx2-turnip-classic",
|
||||
"pcee2",
|
||||
"pcsx2",
|
||||
"pcsx2-legacy",
|
||||
"play",
|
||||
"yaps2"
|
||||
],
|
||||
"sony-psp": [
|
||||
"ppsspp"
|
||||
],
|
||||
"xbox": [
|
||||
"cxbx-reloaded",
|
||||
"directxbox",
|
||||
"hakux",
|
||||
"x1-box",
|
||||
"xemu"
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
{
|
||||
"dest": "colecovision.rom",
|
||||
@@ -648,7 +966,8 @@
|
||||
"size": 16384,
|
||||
"repo_path": "bios/Nintendo/Game Boy Advance/GBA_bios.rom",
|
||||
"cores": [
|
||||
"ares"
|
||||
"ares",
|
||||
"mgba"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1236,7 +1555,11 @@
|
||||
"size": 1224,
|
||||
"repo_path": "bios/Nintendo/Switch/title.keys",
|
||||
"cores": [
|
||||
"citron"
|
||||
"citron",
|
||||
"eden",
|
||||
"ryujinx",
|
||||
"suyu",
|
||||
"yuzu"
|
||||
],
|
||||
"systems": [
|
||||
"nintendo-switch"
|
||||
@@ -1249,7 +1572,11 @@
|
||||
"size": 451,
|
||||
"repo_path": "bios/Other/citron/switch/console.keys",
|
||||
"cores": [
|
||||
"citron"
|
||||
"citron",
|
||||
"eden",
|
||||
"ryujinx",
|
||||
"suyu",
|
||||
"yuzu"
|
||||
],
|
||||
"systems": [
|
||||
"nintendo-switch"
|
||||
@@ -1262,7 +1589,10 @@
|
||||
"size": 160,
|
||||
"repo_path": "bios/Other/citron/switch/key_retail.bin",
|
||||
"cores": [
|
||||
"citron"
|
||||
"citron",
|
||||
"eden",
|
||||
"suyu",
|
||||
"yuzu"
|
||||
],
|
||||
"systems": [
|
||||
"nintendo-switch"
|
||||
@@ -1275,7 +1605,8 @@
|
||||
"size": 8192,
|
||||
"repo_path": "bios/Nintendo/GameCube/.variants/dsp_rom.bin.3c6cc6e0",
|
||||
"cores": [
|
||||
"dolphin"
|
||||
"dolphin",
|
||||
"primehack"
|
||||
],
|
||||
"systems": [
|
||||
"nintendo-gamecube"
|
||||
@@ -1288,7 +1619,8 @@
|
||||
"size": 4096,
|
||||
"repo_path": "bios/Nintendo/GameCube/.variants/dsp_coef.bin.c116d867",
|
||||
"cores": [
|
||||
"dolphin"
|
||||
"dolphin",
|
||||
"primehack"
|
||||
],
|
||||
"systems": [
|
||||
"nintendo-gamecube"
|
||||
@@ -1301,7 +1633,8 @@
|
||||
"size": 6478,
|
||||
"repo_path": "bios/Other/dolphin-mmjr/font_western.bin",
|
||||
"cores": [
|
||||
"dolphin"
|
||||
"dolphin",
|
||||
"primehack"
|
||||
],
|
||||
"systems": [
|
||||
"nintendo-gamecube"
|
||||
@@ -1314,7 +1647,8 @@
|
||||
"size": 259626,
|
||||
"repo_path": "bios/Other/dolphin-mmjr/font_japanese.bin",
|
||||
"cores": [
|
||||
"dolphin"
|
||||
"dolphin",
|
||||
"primehack"
|
||||
],
|
||||
"systems": [
|
||||
"nintendo-gamecube"
|
||||
@@ -1337,7 +1671,8 @@
|
||||
"size": 16384,
|
||||
"repo_path": "bios/Nintendo/Game Boy Advance/GBA_bios.rom",
|
||||
"cores": [
|
||||
"dolphin"
|
||||
"dolphin",
|
||||
"primehack"
|
||||
],
|
||||
"systems": [
|
||||
"nintendo-gamecube"
|
||||
@@ -1350,7 +1685,8 @@
|
||||
"size": 16384,
|
||||
"repo_path": "bios/Nintendo/Wii/SYSCONF",
|
||||
"cores": [
|
||||
"dolphin"
|
||||
"dolphin",
|
||||
"primehack"
|
||||
],
|
||||
"systems": [
|
||||
"nintendo-gamecube"
|
||||
@@ -1363,7 +1699,8 @@
|
||||
"size": 256,
|
||||
"repo_path": "bios/Nintendo/Wii/setting.txt",
|
||||
"cores": [
|
||||
"dolphin"
|
||||
"dolphin",
|
||||
"primehack"
|
||||
],
|
||||
"systems": [
|
||||
"nintendo-gamecube"
|
||||
@@ -1376,7 +1713,8 @@
|
||||
"size": 134217728,
|
||||
"repo_path": "bios/Nintendo/Wii/WiiSD.raw",
|
||||
"cores": [
|
||||
"dolphin"
|
||||
"dolphin",
|
||||
"primehack"
|
||||
],
|
||||
"systems": [
|
||||
"nintendo-gamecube"
|
||||
@@ -1391,7 +1729,8 @@
|
||||
"size": 1005,
|
||||
"repo_path": "bios/Nintendo/Wii/clientca.pem",
|
||||
"cores": [
|
||||
"dolphin"
|
||||
"dolphin",
|
||||
"primehack"
|
||||
],
|
||||
"systems": [
|
||||
"nintendo-gamecube"
|
||||
@@ -1404,7 +1743,8 @@
|
||||
"size": 609,
|
||||
"repo_path": "bios/Nintendo/Wii/clientcakey.pem",
|
||||
"cores": [
|
||||
"dolphin"
|
||||
"dolphin",
|
||||
"primehack"
|
||||
],
|
||||
"systems": [
|
||||
"nintendo-gamecube"
|
||||
@@ -1417,7 +1757,8 @@
|
||||
"size": 897,
|
||||
"repo_path": "bios/Nintendo/Wii/rootca.pem",
|
||||
"cores": [
|
||||
"dolphin"
|
||||
"dolphin",
|
||||
"primehack"
|
||||
],
|
||||
"systems": [
|
||||
"nintendo-gamecube"
|
||||
@@ -2636,7 +2977,8 @@
|
||||
"size": 4194304,
|
||||
"repo_path": "bios/Sony/PlayStation 2/ps2_scph50006.bin",
|
||||
"cores": [
|
||||
"duckstation"
|
||||
"duckstation",
|
||||
"pcsx2"
|
||||
],
|
||||
"systems": [
|
||||
"sony-playstation"
|
||||
@@ -2676,7 +3018,8 @@
|
||||
"size": 4194304,
|
||||
"repo_path": "bios/Sony/PlayStation 2/ps2_scph50008.bin",
|
||||
"cores": [
|
||||
"duckstation"
|
||||
"duckstation",
|
||||
"pcsx2"
|
||||
],
|
||||
"systems": [
|
||||
"sony-playstation"
|
||||
@@ -6534,7 +6877,11 @@
|
||||
"size": 16025,
|
||||
"repo_path": "bios/Nintendo/Switch/prod.keys",
|
||||
"cores": [
|
||||
"citron"
|
||||
"citron",
|
||||
"eden",
|
||||
"ryujinx",
|
||||
"suyu",
|
||||
"yuzu"
|
||||
],
|
||||
"systems": [
|
||||
"nintendo-switch"
|
||||
@@ -6547,7 +6894,8 @@
|
||||
"size": 2097152,
|
||||
"repo_path": "bios/Nintendo/GameCube/GC/USA/IPL.bin",
|
||||
"cores": [
|
||||
"dolphin"
|
||||
"dolphin",
|
||||
"primehack"
|
||||
],
|
||||
"systems": [
|
||||
"nintendo-gamecube"
|
||||
@@ -6566,7 +6914,8 @@
|
||||
"size": 2097152,
|
||||
"repo_path": "bios/Nintendo/GameCube/GC/EUR/IPL.bin",
|
||||
"cores": [
|
||||
"dolphin"
|
||||
"dolphin",
|
||||
"primehack"
|
||||
],
|
||||
"systems": [
|
||||
"nintendo-gamecube"
|
||||
@@ -6585,7 +6934,8 @@
|
||||
"size": 2097152,
|
||||
"repo_path": "bios/Nintendo/GameCube/GC/JAP/IPL.bin",
|
||||
"cores": [
|
||||
"dolphin"
|
||||
"dolphin",
|
||||
"primehack"
|
||||
],
|
||||
"systems": [
|
||||
"nintendo-gamecube"
|
||||
@@ -6604,7 +6954,8 @@
|
||||
"size": 64,
|
||||
"repo_path": "bios/Sony/PlayStation 2/eeprom.dat",
|
||||
"cores": [
|
||||
"armsx2"
|
||||
"armsx2",
|
||||
"pcsx2"
|
||||
],
|
||||
"systems": [
|
||||
"sony-playstation-2"
|
||||
|
||||
+1908
-140
File diff suppressed because it is too large.
Load diff
@@ -5,7 +5,7 @@
|
||||
"platform": "misterfpga",
|
||||
"display_name": "MiSTer FPGA",
|
||||
"version": "1.0",
|
||||
"generated": "2026-10-10T05:24:26Z",
|
||||
"generated": "2026-10-15T21:45:48Z",
|
||||
"base_destination": "games",
|
||||
"detect": [
|
||||
{
|
||||
@@ -22,6 +22,8 @@
|
||||
"pack_size": 29969609,
|
||||
"total_omitted": 0,
|
||||
"omitted_files": [],
|
||||
"core_names": {},
|
||||
"system_cores": {},
|
||||
"files": [
|
||||
{
|
||||
"dest": "3DO/boot.rom",
|
||||
|
||||
+1726
-131
File diff suppressed because it is too large.
Load diff
+1908
-140
File diff suppressed because it is too large.
Load diff
+1301
-399
File diff suppressed because it is too large.
Load diff
+1407
-190
File diff suppressed because it is too large.
Load diff
+2047
-230
File diff suppressed because it is too large.
Load diff
+2055
-243
File diff suppressed because it is too large.
Load diff
+829
-17
@@ -5,7 +5,7 @@
|
||||
"platform": "romm",
|
||||
"display_name": "RomM",
|
||||
"version": "1.0",
|
||||
"generated": "2026-10-10T05:24:26Z",
|
||||
"generated": "2026-10-15T21:45:48Z",
|
||||
"base_destination": "bios",
|
||||
"detect": [
|
||||
{
|
||||
@@ -602,6 +602,793 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"core_names": {
|
||||
"3dsen": [
|
||||
"3dsen-windows"
|
||||
],
|
||||
"ace-dl": [
|
||||
"acedl"
|
||||
],
|
||||
"aethersx2": [
|
||||
"aethersx2-sa"
|
||||
],
|
||||
"armsx2": [
|
||||
"armsx2_libretro"
|
||||
],
|
||||
"beetle_gba": [
|
||||
"mednafen_gba"
|
||||
],
|
||||
"beetle_lynx": [
|
||||
"mednafen_lynx"
|
||||
],
|
||||
"beetle_pce": [
|
||||
"mednafen_pce"
|
||||
],
|
||||
"beetle_pcfx": [
|
||||
"mednafen_pcfx",
|
||||
"pcfx"
|
||||
],
|
||||
"beetle_psx": [
|
||||
"mednafen_psx",
|
||||
"mednafen_psx_hw"
|
||||
],
|
||||
"beetle_saturn": [
|
||||
"beetle-saturn",
|
||||
"mednafen_saturn"
|
||||
],
|
||||
"bsnes2014": [
|
||||
"bsnes2014_accuracy",
|
||||
"bsnes2014_balanced",
|
||||
"bsnes2014_performance"
|
||||
],
|
||||
"bsnes_hd_beta": [
|
||||
"bsnes_hd",
|
||||
"bsneshd"
|
||||
],
|
||||
"bsnes_mercury": [
|
||||
"bsnes_mercury_accuracy",
|
||||
"bsnes_mercury_balanced",
|
||||
"bsnes_mercury_performance"
|
||||
],
|
||||
"cap32": [
|
||||
"caprice32"
|
||||
],
|
||||
"cxbx-reloaded": [
|
||||
"cxbx"
|
||||
],
|
||||
"dosbox-x": [
|
||||
"dosboxx"
|
||||
],
|
||||
"doublecherrygb": [
|
||||
"DoubleCherryGB"
|
||||
],
|
||||
"ep128emu": [
|
||||
"ep128emu_core"
|
||||
],
|
||||
"ep128emu_core": [
|
||||
"ep128emu-core"
|
||||
],
|
||||
"fpse-ng": [
|
||||
"fpse64",
|
||||
"fpseng"
|
||||
],
|
||||
"gba-emu": [
|
||||
"GBA-EMU",
|
||||
"GBA.emu"
|
||||
],
|
||||
"gbc-emu": [
|
||||
"GBC-EMU",
|
||||
"GBC.emu"
|
||||
],
|
||||
"genesis_plus_gx": [
|
||||
"genesisplusgx",
|
||||
"genesisplusgx-expanded",
|
||||
"genplus"
|
||||
],
|
||||
"genesis_plus_gx_wide": [
|
||||
"genesisplusgx-wide",
|
||||
"genesisplusgxwide"
|
||||
],
|
||||
"higan_sfc_balanced": [
|
||||
"nside_sfc_balanced"
|
||||
],
|
||||
"idtech4a++": [
|
||||
"diii4a",
|
||||
"idtech4amm"
|
||||
],
|
||||
"j2me-loader": [
|
||||
"J2ME-LOADER"
|
||||
],
|
||||
"jl-mod": [
|
||||
"JL-MOD"
|
||||
],
|
||||
"kemulator": [
|
||||
"KEMULATOR"
|
||||
],
|
||||
"linkboy": [
|
||||
"LINKBOY",
|
||||
"Linkboy"
|
||||
],
|
||||
"lrps2": [
|
||||
"pcsx2"
|
||||
],
|
||||
"lynx-emu": [
|
||||
"LYNX-EMU",
|
||||
"Lynx.emu"
|
||||
],
|
||||
"m64plus-fz": [
|
||||
"M64PLUS-FZ",
|
||||
"org.mupen64plusae.v3.fzurita",
|
||||
"org.mupen64plusae.v3.fzurita.amazon",
|
||||
"org.mupen64plusae.v3.fzurita.pro"
|
||||
],
|
||||
"mame": [
|
||||
"imame4all"
|
||||
],
|
||||
"mame2016": [
|
||||
"mess2016"
|
||||
],
|
||||
"mame4droid-2024": [
|
||||
"MAME4DROID-2024",
|
||||
"MAME4DROID-CURRENT",
|
||||
"com.seleuco.mame4d2024"
|
||||
],
|
||||
"mastergear": [
|
||||
"MASTERGEAR",
|
||||
"com.fms.mg"
|
||||
],
|
||||
"md-emu": [
|
||||
"MD-EMU",
|
||||
"MD.emu"
|
||||
],
|
||||
"mednafen_pce": [
|
||||
"pce"
|
||||
],
|
||||
"mednafen_pce_fast": [
|
||||
"beetle_pce_fast",
|
||||
"pce_fast"
|
||||
],
|
||||
"mednafen_snes": [
|
||||
"beetle-bsnes",
|
||||
"beetle_bsnes"
|
||||
],
|
||||
"mednafen_supafaust": [
|
||||
"supafaust"
|
||||
],
|
||||
"mednafen_supergrafx": [
|
||||
"beetle_supergrafx"
|
||||
],
|
||||
"melonds_ds": [
|
||||
"melondsds"
|
||||
],
|
||||
"mesen": [
|
||||
"mesens"
|
||||
],
|
||||
"mesen-s": [
|
||||
"mesen_s"
|
||||
],
|
||||
"mesence": [
|
||||
"mesen2"
|
||||
],
|
||||
"msx-emu": [
|
||||
"MSX-EMU",
|
||||
"MSX.emu"
|
||||
],
|
||||
"mupen64plus-ae": [
|
||||
"MUPEN64PLUS-AE",
|
||||
"org.mupen64plusae.v3.alpha"
|
||||
],
|
||||
"mupen64plus_next": [
|
||||
"mupen64plus",
|
||||
"mupen64plus-next",
|
||||
"mupen64plus_next_develop",
|
||||
"mupen64plus_next_gles2",
|
||||
"mupen64plus_next_gles3"
|
||||
],
|
||||
"my-boy": [
|
||||
"MY-BOY",
|
||||
"My Boy!"
|
||||
],
|
||||
"my-oldboy": [
|
||||
"MY-OLDBOY",
|
||||
"My OldBoy!"
|
||||
],
|
||||
"nes-emu": [
|
||||
"NES-EMU",
|
||||
"NES.emu"
|
||||
],
|
||||
"nesoid": [
|
||||
"NESOID",
|
||||
"Nesoid"
|
||||
],
|
||||
"nethersx2": [
|
||||
"NETHERSX2",
|
||||
"NetherSX2"
|
||||
],
|
||||
"nethersx2-turnip": [
|
||||
"NETHERSX2-TURNIP",
|
||||
"NetherSX2-Turnip"
|
||||
],
|
||||
"nethersx2-turnip-classic": [
|
||||
"NETHERSX2-TURNIP-CLASSIC",
|
||||
"NetherSX2-Turnip-Classic"
|
||||
],
|
||||
"o2em": [
|
||||
"o2em_libretro"
|
||||
],
|
||||
"parallel-launcher": [
|
||||
"ca.parallel_launcher.ParallelLauncher"
|
||||
],
|
||||
"parallel_n64": [
|
||||
"parallel_n64_debug"
|
||||
],
|
||||
"pce-emu": [
|
||||
"PCE-EMU",
|
||||
"PCE.emu",
|
||||
"com.PceEmu",
|
||||
"pceemu"
|
||||
],
|
||||
"pcsx2-legacy": [
|
||||
"PCSX2-LEGACY"
|
||||
],
|
||||
"pcsx_rearmed": [
|
||||
"pcsx_rearmed_interpreter",
|
||||
"pcsx_rearmed_neon"
|
||||
],
|
||||
"pizza-boy-gba": [
|
||||
"PIZZA-BOY-GBA",
|
||||
"Pizza Boy GBA"
|
||||
],
|
||||
"pizza-boy-gbc": [
|
||||
"PIZZA-BOY-GBC",
|
||||
"Pizza Boy GBC"
|
||||
],
|
||||
"pizza-boy-sc": [
|
||||
"PIZZA-BOY-SC",
|
||||
"Pizza Boy SC",
|
||||
"it.dbtecno.pizzaboyscbasic",
|
||||
"it.dbtecno.pizzaboyscpro"
|
||||
],
|
||||
"play": [
|
||||
"play!"
|
||||
],
|
||||
"puae": [
|
||||
"uae"
|
||||
],
|
||||
"real3doplayer": [
|
||||
"REAL3DOPLAYER",
|
||||
"Real3DOPlayer"
|
||||
],
|
||||
"rosalies-mupen-gui": [
|
||||
"RMG",
|
||||
"com.github.Rosalie241.RMG"
|
||||
],
|
||||
"saturn-emu": [
|
||||
"SATURN-EMU",
|
||||
"Saturn.emu",
|
||||
"saturnemu"
|
||||
],
|
||||
"smsplus": [
|
||||
"smsplus_gx"
|
||||
],
|
||||
"snes9x": [
|
||||
"pocketsnes",
|
||||
"snes9x_next"
|
||||
],
|
||||
"snes9x-explus": [
|
||||
"SNES9X-EXPLUS",
|
||||
"Snes9x EX+",
|
||||
"Snes9xEXPlus"
|
||||
],
|
||||
"speccy": [
|
||||
"SPECCY",
|
||||
"com.fms.speccy",
|
||||
"com.fms.speccy.deluxe"
|
||||
],
|
||||
"superzsnes": [
|
||||
"SUPERZSNES"
|
||||
],
|
||||
"tgbdual": [
|
||||
"TGB Dual"
|
||||
],
|
||||
"triforce": [
|
||||
"dolphin-triforce"
|
||||
],
|
||||
"vbam": [
|
||||
"vba-m"
|
||||
],
|
||||
"x1-box": [
|
||||
"x1box"
|
||||
],
|
||||
"xm6pro68k": [
|
||||
"XM6PRO68K",
|
||||
"XM6PRO68K-WINDOWS",
|
||||
"xm6pro"
|
||||
],
|
||||
"xm6typeg": [
|
||||
"XM6TYPEG",
|
||||
"XM6TYPEG-WINDOWS"
|
||||
],
|
||||
"yabasanshiro": [
|
||||
"yabasanshiro-2"
|
||||
]
|
||||
},
|
||||
"system_cores": {
|
||||
"3do": [
|
||||
"groovymame",
|
||||
"mame",
|
||||
"mame2015",
|
||||
"mame2016",
|
||||
"mame4droid-2024",
|
||||
"opera",
|
||||
"real3doplayer",
|
||||
"ume2015"
|
||||
],
|
||||
"amstrad-cpc": [
|
||||
"ace-dl",
|
||||
"cap32",
|
||||
"clk",
|
||||
"cpcemu",
|
||||
"crocods",
|
||||
"ep128emu",
|
||||
"ep128emu_core",
|
||||
"zesarux"
|
||||
],
|
||||
"apple-macintosh-ii": [
|
||||
"basiliskii",
|
||||
"minivmac"
|
||||
],
|
||||
"atari-400-800": [
|
||||
"altirra",
|
||||
"atari800"
|
||||
],
|
||||
"atari-5200": [
|
||||
"a5200",
|
||||
"altirra",
|
||||
"atari800"
|
||||
],
|
||||
"atari-7800": [
|
||||
"a7800",
|
||||
"prosystem"
|
||||
],
|
||||
"atari-lynx": [
|
||||
"beetle_lynx",
|
||||
"gearlynx",
|
||||
"handy",
|
||||
"holani",
|
||||
"lynx-emu",
|
||||
"mednafen"
|
||||
],
|
||||
"atari-st": [
|
||||
"clk",
|
||||
"hatari",
|
||||
"hatari2014",
|
||||
"hatarib"
|
||||
],
|
||||
"coleco-colecovision": [
|
||||
"ares",
|
||||
"bluemsx",
|
||||
"clk",
|
||||
"colem",
|
||||
"fbneo",
|
||||
"gearcoleco",
|
||||
"jollycv",
|
||||
"mesence",
|
||||
"msx-emu",
|
||||
"openmsx",
|
||||
"smsplus",
|
||||
"zesarux"
|
||||
],
|
||||
"commodore-amiga": [
|
||||
"amiberry",
|
||||
"clk",
|
||||
"fsuae",
|
||||
"puae",
|
||||
"puae2021",
|
||||
"uae4arm"
|
||||
],
|
||||
"doom": [
|
||||
"gzdoom",
|
||||
"idtech4a++",
|
||||
"prboom",
|
||||
"prboom-plus"
|
||||
],
|
||||
"enterprise-64-128": [
|
||||
"clk"
|
||||
],
|
||||
"fairchild-channel-f": [
|
||||
"fbneo",
|
||||
"freechaf"
|
||||
],
|
||||
"j2me": [
|
||||
"freej2me",
|
||||
"freej2me_plus",
|
||||
"j2me-loader",
|
||||
"jl-mod",
|
||||
"kemulator",
|
||||
"squirreljme"
|
||||
],
|
||||
"magnavox-odyssey2": [
|
||||
"o2em"
|
||||
],
|
||||
"mattel-intellivision": [
|
||||
"freeintv"
|
||||
],
|
||||
"microsoft-msx": [
|
||||
"ares",
|
||||
"bluemsx",
|
||||
"clk",
|
||||
"fbneo",
|
||||
"fmsx",
|
||||
"msx-emu",
|
||||
"openmsx",
|
||||
"zesarux"
|
||||
],
|
||||
"microsoft-xbox": [
|
||||
"cxbx-reloaded",
|
||||
"directxbox",
|
||||
"hakux",
|
||||
"x1-box",
|
||||
"xemu"
|
||||
],
|
||||
"nec-pc-98": [
|
||||
"dosbox-x",
|
||||
"nekop2",
|
||||
"np2kai"
|
||||
],
|
||||
"nec-pc-engine": [
|
||||
"ares",
|
||||
"beetle_pce",
|
||||
"geargrafx",
|
||||
"jgenesis",
|
||||
"mednafen",
|
||||
"mednafen_pce",
|
||||
"mednafen_pce_fast",
|
||||
"mednafen_supergrafx",
|
||||
"mesence",
|
||||
"pce-emu"
|
||||
],
|
||||
"nec-pc-fx": [
|
||||
"beetle_pcfx",
|
||||
"mednafen"
|
||||
],
|
||||
"nintendo-64dd": [
|
||||
"ares",
|
||||
"m64plus-fz",
|
||||
"mupen64plus-ae",
|
||||
"mupen64plus_next",
|
||||
"parallel-launcher",
|
||||
"parallel_n64",
|
||||
"project64",
|
||||
"rosalies-mupen-gui",
|
||||
"sixtyforce"
|
||||
],
|
||||
"nintendo-ds": [
|
||||
"desmume",
|
||||
"desmume2015",
|
||||
"drastic",
|
||||
"melonds",
|
||||
"melonds-nightly",
|
||||
"melonds_ds",
|
||||
"noods",
|
||||
"skyemu"
|
||||
],
|
||||
"nintendo-fds": [
|
||||
"fbneo",
|
||||
"fceumm",
|
||||
"fixnes",
|
||||
"ines",
|
||||
"mednafen",
|
||||
"mesen",
|
||||
"mesence",
|
||||
"nes-emu",
|
||||
"nesoid",
|
||||
"nestopia",
|
||||
"punes",
|
||||
"rustynes"
|
||||
],
|
||||
"nintendo-gamecube": [
|
||||
"dolphin",
|
||||
"dolphin-mmjr",
|
||||
"dolphin-mmjr2",
|
||||
"dolphin_launcher",
|
||||
"ishiiruka",
|
||||
"primehack",
|
||||
"triforce"
|
||||
],
|
||||
"nintendo-gb": [
|
||||
"boytacean",
|
||||
"doublecherrygb",
|
||||
"emux_gb",
|
||||
"gambatte",
|
||||
"gbc-emu",
|
||||
"gearboy",
|
||||
"irogb",
|
||||
"jgenesis",
|
||||
"linkboy",
|
||||
"mednafen",
|
||||
"mesen-s",
|
||||
"mesence",
|
||||
"mgba",
|
||||
"my-oldboy",
|
||||
"pizza-boy-gbc",
|
||||
"sameboy",
|
||||
"skyemu",
|
||||
"tgbdual",
|
||||
"vbam"
|
||||
],
|
||||
"nintendo-gba": [
|
||||
"beetle_gba",
|
||||
"dolphin",
|
||||
"gba-emu",
|
||||
"gpsp",
|
||||
"jgenesis",
|
||||
"linkboy",
|
||||
"mednafen",
|
||||
"mednafen_gba",
|
||||
"mesence",
|
||||
"meteor",
|
||||
"mgba",
|
||||
"mgba_splitscreen",
|
||||
"my-boy",
|
||||
"noods",
|
||||
"pizza-boy-gba",
|
||||
"primehack",
|
||||
"skyemu",
|
||||
"tempgba",
|
||||
"triforce",
|
||||
"vba_next",
|
||||
"vbam"
|
||||
],
|
||||
"nintendo-gbc": [
|
||||
"boytacean",
|
||||
"doublecherrygb",
|
||||
"fixgb",
|
||||
"gambatte",
|
||||
"gbc-emu",
|
||||
"gearboy",
|
||||
"irogb",
|
||||
"jgenesis",
|
||||
"linkboy",
|
||||
"mednafen",
|
||||
"mesen-s",
|
||||
"mesence",
|
||||
"mgba",
|
||||
"my-oldboy",
|
||||
"pizza-boy-gbc",
|
||||
"sameboy",
|
||||
"skyemu",
|
||||
"tgbdual",
|
||||
"vbam"
|
||||
],
|
||||
"nintendo-nes": [
|
||||
"3dsen",
|
||||
"bnes",
|
||||
"emux_nes",
|
||||
"fceumm",
|
||||
"fixnes",
|
||||
"ines",
|
||||
"jgenesis",
|
||||
"mednafen",
|
||||
"mesen",
|
||||
"mesence",
|
||||
"nes",
|
||||
"nes-emu",
|
||||
"nesoid",
|
||||
"nestopia",
|
||||
"plastic",
|
||||
"punes",
|
||||
"quicknes",
|
||||
"rustynes"
|
||||
],
|
||||
"nintendo-pokemon-mini": [
|
||||
"pokemini"
|
||||
],
|
||||
"nintendo-satellaview": [
|
||||
"bsnes",
|
||||
"bsnes-jg",
|
||||
"bsnes2014",
|
||||
"bsnes_cplusplus98",
|
||||
"bsnes_hd_beta",
|
||||
"bsnes_mercury",
|
||||
"chimerasnes",
|
||||
"mesen-s",
|
||||
"mesence",
|
||||
"snes9x",
|
||||
"snes9x-explus",
|
||||
"snes9x2002",
|
||||
"snes9x2005",
|
||||
"snes9x2005_plus",
|
||||
"snes9x2010"
|
||||
],
|
||||
"nintendo-sgb": [
|
||||
"bsnes-jg",
|
||||
"bsnes2014",
|
||||
"bsnes_hd_beta",
|
||||
"bsnes_mercury",
|
||||
"higan_sfc",
|
||||
"higan_sfc_balanced",
|
||||
"linkboy",
|
||||
"mgba",
|
||||
"my-oldboy",
|
||||
"pizza-boy-gbc",
|
||||
"sameboy"
|
||||
],
|
||||
"nintendo-snes": [
|
||||
"bsnes",
|
||||
"bsnes-jg",
|
||||
"bsnes2014",
|
||||
"bsnes_cplusplus98",
|
||||
"bsnes_hd_beta",
|
||||
"bsnes_mercury",
|
||||
"chimerasnes",
|
||||
"fbneo",
|
||||
"higan_sfc",
|
||||
"higan_sfc_balanced",
|
||||
"jgenesis",
|
||||
"mednafen",
|
||||
"mednafen_snes",
|
||||
"mednafen_supafaust",
|
||||
"mesen-s",
|
||||
"mesence",
|
||||
"snes9x",
|
||||
"snes9x-explus",
|
||||
"snes9x2002",
|
||||
"snes9x2005",
|
||||
"snes9x2005_plus",
|
||||
"snes9x2010",
|
||||
"superzsnes"
|
||||
],
|
||||
"nintendo-sufami-turbo": [
|
||||
"bsnes-jg",
|
||||
"bsnes2014",
|
||||
"bsnes_cplusplus98",
|
||||
"bsnes_hd_beta",
|
||||
"bsnes_mercury",
|
||||
"chimerasnes",
|
||||
"mesence",
|
||||
"snes9x",
|
||||
"snes9x-explus",
|
||||
"snes9x2002",
|
||||
"snes9x2005",
|
||||
"snes9x2005_plus",
|
||||
"snes9x2010"
|
||||
],
|
||||
"philips-videopac": [
|
||||
"o2em"
|
||||
],
|
||||
"scummvm": [
|
||||
"scummvm"
|
||||
],
|
||||
"sega-game-gear": [
|
||||
"ares",
|
||||
"gearsystem",
|
||||
"genesis_plus_gx",
|
||||
"genesis_plus_gx_wide",
|
||||
"jgenesis",
|
||||
"mastergear",
|
||||
"mednafen",
|
||||
"mesence",
|
||||
"picodrive",
|
||||
"pizza-boy-sc",
|
||||
"smsplus"
|
||||
],
|
||||
"sega-master-system": [
|
||||
"ares",
|
||||
"blastem",
|
||||
"clk",
|
||||
"gearsystem",
|
||||
"genesis_plus_gx",
|
||||
"genesis_plus_gx_wide",
|
||||
"jgenesis",
|
||||
"mastergear",
|
||||
"md-emu",
|
||||
"mednafen",
|
||||
"mesence",
|
||||
"picodrive",
|
||||
"pizza-boy-sc",
|
||||
"smsplus",
|
||||
"zesarux"
|
||||
],
|
||||
"sega-mega-cd": [
|
||||
"ares",
|
||||
"clownmdemu",
|
||||
"genesis_plus_gx",
|
||||
"genesis_plus_gx_wide",
|
||||
"jgenesis",
|
||||
"md-emu",
|
||||
"mednafen",
|
||||
"picodrive",
|
||||
"pizza-boy-sc"
|
||||
],
|
||||
"sega-mega-drive": [
|
||||
"ares",
|
||||
"blastem",
|
||||
"clownmdemu",
|
||||
"genesis_plus_gx",
|
||||
"genesis_plus_gx_wide",
|
||||
"jgenesis",
|
||||
"md-emu",
|
||||
"mednafen",
|
||||
"picodrive",
|
||||
"pizza-boy-sc"
|
||||
],
|
||||
"sega-saturn": [
|
||||
"ares",
|
||||
"beetle_saturn",
|
||||
"kronos",
|
||||
"mednafen",
|
||||
"saturn-emu",
|
||||
"ssf",
|
||||
"yabasanshiro",
|
||||
"yabause",
|
||||
"ymir"
|
||||
],
|
||||
"sharp-x1": [
|
||||
"x1"
|
||||
],
|
||||
"sharp-x68000": [
|
||||
"px68k",
|
||||
"xm6pro68k",
|
||||
"xm6typeg"
|
||||
],
|
||||
"sinclair-zx-spectrum": [
|
||||
"81",
|
||||
"ares",
|
||||
"clk",
|
||||
"cspect",
|
||||
"ep128emu",
|
||||
"ep128emu_core",
|
||||
"fuse",
|
||||
"speccy",
|
||||
"zesarux"
|
||||
],
|
||||
"snk-neogeo-cd": [
|
||||
"fbneo",
|
||||
"fbneo_neogeo",
|
||||
"geolith",
|
||||
"neocd"
|
||||
],
|
||||
"sony-playstation": [
|
||||
"ares",
|
||||
"armsx1",
|
||||
"beetle_psx",
|
||||
"duckstation",
|
||||
"epsxe",
|
||||
"fpse",
|
||||
"fpse-ng",
|
||||
"mednafen",
|
||||
"pcsx1",
|
||||
"pcsx_rearmed",
|
||||
"rustation",
|
||||
"swanstation"
|
||||
],
|
||||
"sony-playstation-2": [
|
||||
"aethersx2",
|
||||
"armsx2",
|
||||
"emucorex",
|
||||
"lrps2",
|
||||
"nethersx2",
|
||||
"nethersx2-turnip",
|
||||
"nethersx2-turnip-classic",
|
||||
"pcee2",
|
||||
"pcsx2",
|
||||
"pcsx2-legacy",
|
||||
"play",
|
||||
"yaps2"
|
||||
],
|
||||
"sony-psp": [
|
||||
"ppsspp"
|
||||
],
|
||||
"videoton-tvc": [
|
||||
"ep128emu",
|
||||
"ep128emu_core"
|
||||
],
|
||||
"wolfenstein-3d": [
|
||||
"ecwolf",
|
||||
"idtech4a++"
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
{
|
||||
"dest": "3do/3do_arcade_saot.bin",
|
||||
@@ -4892,7 +5679,8 @@
|
||||
"size": 524288,
|
||||
"repo_path": "bios/Sony/PlayStation/psxonpsp660.bin",
|
||||
"cores": [
|
||||
"beetle_psx"
|
||||
"beetle_psx",
|
||||
"pcsx_rearmed"
|
||||
],
|
||||
"systems": [
|
||||
"sony-playstation"
|
||||
@@ -6420,7 +7208,9 @@
|
||||
"size": 2097152,
|
||||
"repo_path": "bios/Nintendo/GameCube/GC/USA/IPL.bin",
|
||||
"cores": [
|
||||
"dolphin"
|
||||
"dolphin",
|
||||
"ishiiruka",
|
||||
"primehack"
|
||||
],
|
||||
"systems": [
|
||||
"nintendo-gamecube"
|
||||
@@ -6439,7 +7229,9 @@
|
||||
"size": 2097152,
|
||||
"repo_path": "bios/Nintendo/GameCube/GC/EUR/IPL.bin",
|
||||
"cores": [
|
||||
"dolphin"
|
||||
"dolphin",
|
||||
"ishiiruka",
|
||||
"primehack"
|
||||
],
|
||||
"systems": [
|
||||
"nintendo-gamecube"
|
||||
@@ -6458,7 +7250,9 @@
|
||||
"size": 2097152,
|
||||
"repo_path": "bios/Nintendo/GameCube/GC/JAP/IPL.bin",
|
||||
"cores": [
|
||||
"dolphin"
|
||||
"dolphin",
|
||||
"ishiiruka",
|
||||
"primehack"
|
||||
],
|
||||
"systems": [
|
||||
"nintendo-gamecube"
|
||||
@@ -6477,7 +7271,9 @@
|
||||
"size": 8192,
|
||||
"repo_path": "bios/Nintendo/GameCube/.variants/dsp_rom.bin.3c6cc6e0",
|
||||
"cores": [
|
||||
"dolphin"
|
||||
"dolphin",
|
||||
"ishiiruka",
|
||||
"primehack"
|
||||
],
|
||||
"systems": [
|
||||
"nintendo-gamecube"
|
||||
@@ -6490,7 +7286,9 @@
|
||||
"size": 4096,
|
||||
"repo_path": "bios/Nintendo/GameCube/.variants/dsp_coef.bin.c116d867",
|
||||
"cores": [
|
||||
"dolphin"
|
||||
"dolphin",
|
||||
"ishiiruka",
|
||||
"primehack"
|
||||
],
|
||||
"systems": [
|
||||
"nintendo-gamecube"
|
||||
@@ -6503,7 +7301,9 @@
|
||||
"size": 6478,
|
||||
"repo_path": "bios/Other/dolphin-mmjr/font_western.bin",
|
||||
"cores": [
|
||||
"dolphin"
|
||||
"dolphin",
|
||||
"ishiiruka",
|
||||
"primehack"
|
||||
],
|
||||
"systems": [
|
||||
"nintendo-gamecube"
|
||||
@@ -6516,7 +7316,9 @@
|
||||
"size": 259626,
|
||||
"repo_path": "bios/Other/dolphin-mmjr/font_japanese.bin",
|
||||
"cores": [
|
||||
"dolphin"
|
||||
"dolphin",
|
||||
"ishiiruka",
|
||||
"primehack"
|
||||
],
|
||||
"systems": [
|
||||
"nintendo-gamecube"
|
||||
@@ -6539,7 +7341,9 @@
|
||||
"size": 16384,
|
||||
"repo_path": "bios/Nintendo/Wii/SYSCONF",
|
||||
"cores": [
|
||||
"dolphin"
|
||||
"dolphin",
|
||||
"ishiiruka",
|
||||
"primehack"
|
||||
],
|
||||
"systems": [
|
||||
"nintendo-gamecube"
|
||||
@@ -6552,7 +7356,9 @@
|
||||
"size": 256,
|
||||
"repo_path": "bios/Nintendo/Wii/setting.txt",
|
||||
"cores": [
|
||||
"dolphin"
|
||||
"dolphin",
|
||||
"ishiiruka",
|
||||
"primehack"
|
||||
],
|
||||
"systems": [
|
||||
"nintendo-gamecube"
|
||||
@@ -6565,7 +7371,8 @@
|
||||
"size": 134217728,
|
||||
"repo_path": "bios/Nintendo/Wii/WiiSD.raw",
|
||||
"cores": [
|
||||
"dolphin"
|
||||
"dolphin",
|
||||
"primehack"
|
||||
],
|
||||
"systems": [
|
||||
"nintendo-gamecube"
|
||||
@@ -6580,7 +7387,8 @@
|
||||
"size": 1005,
|
||||
"repo_path": "bios/Nintendo/Wii/clientca.pem",
|
||||
"cores": [
|
||||
"dolphin"
|
||||
"dolphin",
|
||||
"primehack"
|
||||
],
|
||||
"systems": [
|
||||
"nintendo-gamecube"
|
||||
@@ -6593,7 +7401,8 @@
|
||||
"size": 609,
|
||||
"repo_path": "bios/Nintendo/Wii/clientcakey.pem",
|
||||
"cores": [
|
||||
"dolphin"
|
||||
"dolphin",
|
||||
"primehack"
|
||||
],
|
||||
"systems": [
|
||||
"nintendo-gamecube"
|
||||
@@ -6606,7 +7415,8 @@
|
||||
"size": 897,
|
||||
"repo_path": "bios/Nintendo/Wii/rootca.pem",
|
||||
"cores": [
|
||||
"dolphin"
|
||||
"dolphin",
|
||||
"primehack"
|
||||
],
|
||||
"systems": [
|
||||
"nintendo-gamecube"
|
||||
@@ -6619,7 +7429,8 @@
|
||||
"size": 32768,
|
||||
"repo_path": "bios/Enterprise/64-128/exdos14isdos10uk.rom",
|
||||
"cores": [
|
||||
"ep128emu"
|
||||
"ep128emu",
|
||||
"ep128emu_core"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -6629,7 +7440,8 @@
|
||||
"size": 32768,
|
||||
"repo_path": "bios/Enterprise/64-128/epdos16f.rom",
|
||||
"cores": [
|
||||
"ep128emu"
|
||||
"ep128emu",
|
||||
"ep128emu_core"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -96,6 +96,30 @@
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"core_names": {
|
||||
"description": "Each core a file names, with the other names it goes by upstream (the buildbot's mednafen_psx for beetle_psx).",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"uniqueItems": true
|
||||
}
|
||||
},
|
||||
"system_cores": {
|
||||
"description": "Each platform system with the cores whose presence keeps it; a system absent here is kept whatever cores are available.",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"uniqueItems": true
|
||||
}
|
||||
},
|
||||
"omitted_files": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -377,6 +401,53 @@
|
||||
"priority": {
|
||||
"description": "The rank the code searches the file at, lower first: a world file withdraws an unmatched regional one only when it ranks no worse.",
|
||||
"type": "integer"
|
||||
},
|
||||
"alternatives": {
|
||||
"description": "Copies other cores read at this destination when they disagree on its content, in the order the pack builder tries them; a narrowing that drops the entry's cores takes the first whose cores remain.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"sha1",
|
||||
"sha256",
|
||||
"size",
|
||||
"repo_path",
|
||||
"cores"
|
||||
],
|
||||
"properties": {
|
||||
"sha1": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-f0-9]{40}$"
|
||||
},
|
||||
"sha256": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-f0-9]{64}$"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"repo_path": {
|
||||
"type": "string"
|
||||
},
|
||||
"storage": {
|
||||
"const": "release"
|
||||
},
|
||||
"release_asset": {
|
||||
"type": "string",
|
||||
"pattern": "^[^/\\\\]+$"
|
||||
},
|
||||
"cores": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"minItems": 1,
|
||||
"uniqueItems": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
|
||||
+53
-54
@@ -1518,15 +1518,7 @@ def resolve_platform_cores(
|
||||
}
|
||||
|
||||
if target_cores is not None:
|
||||
# Build reverse index: upstream name -> profile key
|
||||
# Upstream sources (buildbot, es_systems) may use different names
|
||||
# than our profile keys (e.g., mednafen_psx vs beetle_psx).
|
||||
# The profiles' cores: field lists these alternate names.
|
||||
index = upstream_profile_index(profiles, include_aliases=True)
|
||||
expanded = {
|
||||
profile for c in target_cores for profile in index.get(c, {c})
|
||||
}
|
||||
result = result & expanded
|
||||
result = result & designated_profiles(profiles, target_cores)
|
||||
return result
|
||||
|
||||
|
||||
@@ -1617,40 +1609,39 @@ def _norm_system_id(sid: str) -> str:
|
||||
return s.replace("-", "")
|
||||
|
||||
|
||||
def filter_systems_by_target(
|
||||
def designated_profiles(profiles: dict[str, dict], names: set[str]) -> set[str]:
|
||||
"""The profiles a list of upstream core names designates.
|
||||
|
||||
The buildbot and es_systems name cores their own way (`mednafen_psx` for
|
||||
beetle_psx); a profile's `cores:` lists those names. A name no profile
|
||||
claims stands for itself.
|
||||
"""
|
||||
index = upstream_profile_index(profiles, include_aliases=True)
|
||||
return {profile for c in names for profile in index.get(c, {c})}
|
||||
|
||||
|
||||
def system_core_reach(
|
||||
systems: dict[str, dict],
|
||||
profiles: dict[str, dict],
|
||||
target_cores: set[str] | None,
|
||||
platform_cores: set[str] | None = None,
|
||||
) -> dict[str, dict]:
|
||||
"""Filter platform systems to only those reachable by target cores.
|
||||
platform_cores: set[str] | None,
|
||||
) -> dict[str, set[str] | None]:
|
||||
"""For each platform system, the cores whose presence keeps it.
|
||||
|
||||
A system is reachable if at least one core that emulates it is available
|
||||
on the target. Only considers cores relevant to the platform (from
|
||||
platform_cores). Systems whose cores are all outside the platform's
|
||||
scope are kept (no information to exclude them).
|
||||
|
||||
Returns the filtered systems dict (or all if no target).
|
||||
A system is reachable if at least one core that emulates it is available.
|
||||
Only cores relevant to the platform (platform_cores) can rule a system
|
||||
out: a system whose cores are all outside the platform's scope maps to
|
||||
None, kept whatever is available, there being no information to exclude
|
||||
it.
|
||||
"""
|
||||
if target_cores is None:
|
||||
return systems
|
||||
|
||||
# Build reverse index for target core name resolution
|
||||
index = upstream_profile_index(profiles, include_aliases=True)
|
||||
expanded_target = {
|
||||
profile for c in target_cores for profile in index.get(c, {c})
|
||||
}
|
||||
|
||||
_norm_sid = _norm_system_id
|
||||
|
||||
# Build normalized system -> cores from ALL profiles
|
||||
# Normalized system -> cores, from ALL profiles
|
||||
norm_system_cores: dict[str, set[str]] = {}
|
||||
for name, p in profiles.items():
|
||||
if p.get("type") == "alias":
|
||||
continue
|
||||
for sid in p.get("systems", []):
|
||||
norm_key = _norm_sid(sid)
|
||||
norm_system_cores.setdefault(norm_key, set()).add(name)
|
||||
norm_system_cores.setdefault(_norm_sid(sid), set()).add(name)
|
||||
|
||||
# Platform-scoped mapping (for distinguishing "no info" from "known but off-target")
|
||||
norm_plat_system_cores: dict[str, set[str]] = {}
|
||||
@@ -1658,14 +1649,16 @@ def filter_systems_by_target(
|
||||
for name in platform_cores:
|
||||
p = profiles.get(name, {})
|
||||
for sid in p.get("systems", []):
|
||||
norm_key = _norm_sid(sid)
|
||||
norm_plat_system_cores.setdefault(norm_key, set()).add(name)
|
||||
norm_plat_system_cores.setdefault(_norm_sid(sid), set()).add(name)
|
||||
|
||||
# A platform can file a system under an id no profile uses: Batocera's
|
||||
# enterprise-64-128 is ep128emu's enterprise-64 and enterprise-128, and
|
||||
# only CLK, off most targets, carries the joined id. The destinations a
|
||||
# system declares name the cores that read its files. A bare name proves
|
||||
# nothing (bios.bin belongs to many systems), so only a path counts.
|
||||
# The evidence only keeps: a path shared with an off-target core says
|
||||
# nothing of the other files, and Batocera's bbc runs on MAME, which no
|
||||
# profile ties to bbcb.zip, while CLK reads two of its ROMs.
|
||||
dest_cores: dict[str, set[str]] = {}
|
||||
for name, p in profiles.items():
|
||||
if p.get("type") == "alias":
|
||||
@@ -1681,9 +1674,12 @@ def filter_systems_by_target(
|
||||
if "/" in dest:
|
||||
dest_cores.setdefault(dest, set()).add(name)
|
||||
|
||||
filtered = {}
|
||||
reach: dict[str, set[str] | None] = {}
|
||||
for sys_id, sys_data in systems.items():
|
||||
norm_key = _norm_sid(sys_id)
|
||||
if not norm_plat_system_cores.get(norm_key):
|
||||
reach[sys_id] = None
|
||||
continue
|
||||
file_cores = {
|
||||
core
|
||||
for fe in sys_data.get("files", [])
|
||||
@@ -1692,26 +1688,29 @@ def filter_systems_by_target(
|
||||
(),
|
||||
)
|
||||
}
|
||||
all_cores = norm_system_cores.get(norm_key, set())
|
||||
plat_cores_here = norm_plat_system_cores.get(norm_key, set())
|
||||
reach[sys_id] = file_cores | norm_system_cores.get(norm_key, set())
|
||||
return reach
|
||||
|
||||
if file_cores & expanded_target:
|
||||
# An on-target core reads the system's files. The evidence only
|
||||
# keeps: a path shared with an off-target core says nothing of the
|
||||
# other files, and Batocera's bbc runs on MAME, which no profile
|
||||
# ties to bbcb.zip, while CLK reads two of its ROMs.
|
||||
filtered[sys_id] = sys_data
|
||||
elif not all_cores and not plat_cores_here:
|
||||
# No profile maps to this system -keep it
|
||||
filtered[sys_id] = sys_data
|
||||
elif all_cores & expanded_target:
|
||||
# At least one core is on the target
|
||||
filtered[sys_id] = sys_data
|
||||
elif not plat_cores_here:
|
||||
# Platform resolution didn't find cores for this system -keep it
|
||||
filtered[sys_id] = sys_data
|
||||
# else: known cores exist but none are on the target -exclude
|
||||
return filtered
|
||||
|
||||
def filter_systems_by_target(
|
||||
systems: dict[str, dict],
|
||||
profiles: dict[str, dict],
|
||||
target_cores: set[str] | None,
|
||||
platform_cores: set[str] | None = None,
|
||||
) -> dict[str, dict]:
|
||||
"""Filter platform systems to only those reachable by target cores.
|
||||
|
||||
Returns the filtered systems dict (or all if no target).
|
||||
"""
|
||||
if target_cores is None:
|
||||
return systems
|
||||
available = designated_profiles(profiles, target_cores)
|
||||
reach = system_core_reach(systems, profiles, platform_cores)
|
||||
return {
|
||||
sys_id: sys_data
|
||||
for sys_id, sys_data in systems.items()
|
||||
if reach[sys_id] is None or reach[sys_id] & available
|
||||
}
|
||||
|
||||
|
||||
def platform_declarations(
|
||||
|
||||
@@ -60,6 +60,8 @@ from common import (
|
||||
resolve_local_file,
|
||||
sanitize_pack_path,
|
||||
select_emulator_profiles,
|
||||
system_core_reach,
|
||||
upstream_profile_index,
|
||||
yaml_load,
|
||||
)
|
||||
import packresolve
|
||||
@@ -3236,6 +3238,46 @@ def _manifest_core_entries(
|
||||
pack_only_sizes.append(file_size)
|
||||
continue
|
||||
|
||||
# Every other core that reads this destination, in the order the
|
||||
# builder meets them: a pack built without the earlier cores carries
|
||||
# the first remaining one's copy. A claim joins the copy just before
|
||||
# it when the bytes agree, and starts an alternative otherwise; it
|
||||
# never joins an earlier copy, which would let it win over a core
|
||||
# the builder tries first. Only a core's first claim can win: its
|
||||
# later ones are reached only when that one is.
|
||||
claimed = set(entry["cores"])
|
||||
for claim in fe.get("other_claims", []):
|
||||
claim_emu = claim.get("source_profile") or claim.get("source_emulator", "")
|
||||
if not claim_emu or claim_emu in claimed:
|
||||
continue
|
||||
claimed.add(claim_emu)
|
||||
if required_only and claim.get("required") is False:
|
||||
continue
|
||||
claim_path, claim_status = resolve_file(
|
||||
claim, db, bios_dir, zip_contents,
|
||||
dest_hint=claim.get("destination", ""), offline=offline,
|
||||
)
|
||||
if claim_status in ("not_found", "external", "user_provided") or not claim_path:
|
||||
continue
|
||||
claim_path, _ = validated_choice(
|
||||
claim, claim_path, db, validation_index or {}, bios_dir, None, dest, owners,
|
||||
)
|
||||
copy, _size = _manifest_entry(
|
||||
claim_path, manifest_dest, [claim_emu], db, repo_root, {}
|
||||
)
|
||||
if copy is None:
|
||||
continue
|
||||
alternatives = entry.setdefault("alternatives", [])
|
||||
last = alternatives[-1] if alternatives else entry
|
||||
if copy["sha1"] == last["sha1"]:
|
||||
if claim_emu not in last["cores"]:
|
||||
last["cores"].append(claim_emu)
|
||||
continue
|
||||
del copy["dest"]
|
||||
alternatives.append(copy)
|
||||
if not entry.get("alternatives"):
|
||||
entry.pop("alternatives", None)
|
||||
|
||||
manifest_files.append(entry)
|
||||
omitted_by_destination.pop(full_dest, None)
|
||||
total_size += file_size
|
||||
@@ -3553,6 +3595,9 @@ def generate_manifest(
|
||||
]
|
||||
|
||||
now = _build_timestamp(db)
|
||||
core_names, system_cores = _manifest_core_index(
|
||||
config, emu_profiles, manifest_files, omitted_by_destination.values()
|
||||
)
|
||||
|
||||
result: dict = {
|
||||
"manifest_version": 2,
|
||||
@@ -3581,11 +3626,50 @@ def generate_manifest(
|
||||
"omitted_files": sorted(
|
||||
omitted_by_destination.values(), key=lambda entry: entry["dest"]
|
||||
),
|
||||
"core_names": core_names,
|
||||
"system_cores": system_cores,
|
||||
"files": manifest_files,
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def _manifest_core_index(
|
||||
config: dict,
|
||||
profiles: dict[str, dict],
|
||||
files: list[dict],
|
||||
omitted,
|
||||
) -> tuple[dict[str, list[str]], dict[str, list[str]]]:
|
||||
"""What the installer needs to narrow by a list of core names.
|
||||
|
||||
A target list, or the cores found installed, names cores the way the
|
||||
buildbot does, and a file names the profile that reads it: `core_names`
|
||||
gives each profile the other names it goes by. `system_cores` gives each
|
||||
platform system the cores whose presence keeps it, as
|
||||
`filter_systems_by_target` decides; a system absent from it is kept
|
||||
whatever is available.
|
||||
"""
|
||||
reach = system_core_reach(
|
||||
config.get("systems", {}), profiles, _platform_cores(config, profiles)
|
||||
)
|
||||
system_cores = {
|
||||
sys_id: sorted(cores) for sys_id, cores in sorted(reach.items())
|
||||
if cores is not None
|
||||
}
|
||||
named = {
|
||||
c for entry in [*files, *omitted]
|
||||
for copy in [entry, *entry.get("alternatives", [])]
|
||||
for c in copy.get("cores") or []
|
||||
}
|
||||
named |= {c for cores in system_cores.values() for c in cores}
|
||||
aliases: dict[str, set[str]] = {}
|
||||
for name, claimants in upstream_profile_index(profiles, include_aliases=True).items():
|
||||
for profile in claimants & named:
|
||||
if name != profile:
|
||||
aliases.setdefault(profile, set()).add(name)
|
||||
core_names = {profile: sorted(names) for profile, names in sorted(aliases.items())}
|
||||
return core_names, system_cores
|
||||
|
||||
|
||||
# Post-generation pack verification + manifest + SHA256SUMS
|
||||
|
||||
|
||||
|
||||
+132
-80
@@ -158,6 +158,7 @@ def _agnostic_scan_extras(
|
||||
seen_dests: set,
|
||||
extras_prefix: str,
|
||||
standalone_set: set[str] | None = None,
|
||||
taken: dict[str, dict] | None = None,
|
||||
) -> list[dict]:
|
||||
"""Every interchangeable candidate a filename-agnostic core accepts.
|
||||
|
||||
@@ -261,33 +262,34 @@ def _agnostic_scan_extras(
|
||||
folder = seed.rsplit("/", 1)[0] if "/" in seed else ""
|
||||
dest = f"{folder}/{scan_name}" if folder else scan_name
|
||||
full_dest = f"{extras_prefix}/{dest}" if extras_prefix else dest
|
||||
if full_dest in seen_dests:
|
||||
if full_dest in seen_dests and (taken is None or full_dest not in taken):
|
||||
continue
|
||||
seen_dests.add(full_dest)
|
||||
extras.append(
|
||||
{
|
||||
"name": scan_name,
|
||||
"destination": dest,
|
||||
# The scan already holds the entry it selected, so it
|
||||
# names it by content. Emitting the filename alone
|
||||
# sent the packing step back to a by-name lookup, and
|
||||
# three PS2 resources came back from another
|
||||
# emulator's copy of the same filename.
|
||||
"sha1": sha1,
|
||||
"required": False,
|
||||
"hle_fallback": False,
|
||||
"source_emulator": profile.get("emulator", emu_name),
|
||||
"source_profile": emu_name,
|
||||
"source_system": f.get("system"),
|
||||
"source_systems": list(profile.get("systems", [])),
|
||||
"region": f.get("region"),
|
||||
"variant_group": f.get("variant_group"),
|
||||
"agnostic_scan": True,
|
||||
}
|
||||
)
|
||||
extra = {
|
||||
"name": scan_name,
|
||||
"destination": dest,
|
||||
# The scan already holds the entry it selected, so it
|
||||
# names it by content. Emitting the filename alone
|
||||
# sent the packing step back to a by-name lookup, and
|
||||
# three PS2 resources came back from another
|
||||
# emulator's copy of the same filename.
|
||||
"sha1": sha1,
|
||||
"required": False,
|
||||
"hle_fallback": False,
|
||||
"source_emulator": profile.get("emulator", emu_name),
|
||||
"source_profile": emu_name,
|
||||
"source_system": f.get("system"),
|
||||
"source_systems": list(profile.get("systems", [])),
|
||||
"region": f.get("region"),
|
||||
"variant_group": f.get("variant_group"),
|
||||
"agnostic_scan": True,
|
||||
}
|
||||
extra["claim_order"] = (4, 0, emu_name)
|
||||
if not _take(extra, full_dest, seen_dests, taken):
|
||||
continue
|
||||
extras.append(extra)
|
||||
extras.extend(_companion_extras(
|
||||
_companions(path, scan_name, _companion_extensions(profile, fname), files_db),
|
||||
folder, extras_prefix, seen_dests, profile, emu_name, f,
|
||||
folder, extras_prefix, seen_dests, profile, emu_name, f, taken,
|
||||
))
|
||||
return extras
|
||||
|
||||
@@ -300,16 +302,16 @@ def _companion_extras(
|
||||
profile: dict,
|
||||
emu_name: str,
|
||||
seed: dict,
|
||||
taken: dict[str, dict] | None = None,
|
||||
) -> list[dict]:
|
||||
"""Extras for the companions of one scanned image, placed beside it."""
|
||||
extras: list[dict] = []
|
||||
for companion_sha1, companion in companions:
|
||||
companion_dest = f"{folder}/{companion['name']}" if folder else companion["name"]
|
||||
companion_full = f"{extras_prefix}/{companion_dest}" if extras_prefix else companion_dest
|
||||
if companion_full in seen_dests:
|
||||
if companion_full in seen_dests and (taken is None or companion_full not in taken):
|
||||
continue
|
||||
seen_dests.add(companion_full)
|
||||
extras.append({
|
||||
extra = {
|
||||
"name": companion["name"],
|
||||
"destination": companion_dest,
|
||||
"sha1": companion_sha1,
|
||||
@@ -322,7 +324,10 @@ def _companion_extras(
|
||||
"region": seed.get("region"),
|
||||
"variant_group": seed.get("variant_group"),
|
||||
"agnostic_scan": True,
|
||||
})
|
||||
}
|
||||
extra["claim_order"] = (4, 0, emu_name)
|
||||
if _take(extra, companion_full, seen_dests, taken):
|
||||
extras.append(extra)
|
||||
return extras
|
||||
|
||||
|
||||
@@ -368,6 +373,7 @@ def _archive_prefix_extras(
|
||||
seen_dests: set,
|
||||
extras_prefix: str,
|
||||
by_name: dict,
|
||||
taken: dict[str, dict] | None = None,
|
||||
) -> list[dict]:
|
||||
"""A second copy of an archive under the subdirectory a core reads.
|
||||
|
||||
@@ -398,25 +404,25 @@ def _archive_prefix_extras(
|
||||
continue
|
||||
dest = f"{prefix}/{archive_name}"
|
||||
full_dest = f"{extras_prefix}/{dest}" if extras_prefix else dest
|
||||
if full_dest in seen_dests:
|
||||
if full_dest in seen_dests and (taken is None or full_dest not in taken):
|
||||
continue
|
||||
if not by_name.get(archive_name):
|
||||
continue
|
||||
seen_dests.add(full_dest)
|
||||
extras.append(
|
||||
{
|
||||
"name": archive_name,
|
||||
"destination": dest,
|
||||
"required": True,
|
||||
"hle_fallback": False,
|
||||
"source_emulator": profile.get("emulator", emu_name),
|
||||
"source_profile": emu_name,
|
||||
"source_system": archive_entry.get("system"),
|
||||
"source_systems": list(profile.get("systems", [])),
|
||||
"region": archive_entry.get("region"),
|
||||
"variant_group": archive_entry.get("variant_group"),
|
||||
}
|
||||
)
|
||||
extra = {
|
||||
"name": archive_name,
|
||||
"destination": dest,
|
||||
"required": True,
|
||||
"hle_fallback": False,
|
||||
"source_emulator": profile.get("emulator", emu_name),
|
||||
"source_profile": emu_name,
|
||||
"source_system": archive_entry.get("system"),
|
||||
"source_systems": list(profile.get("systems", [])),
|
||||
"region": archive_entry.get("region"),
|
||||
"variant_group": archive_entry.get("variant_group"),
|
||||
}
|
||||
extra["claim_order"] = (3, 0, emu_name)
|
||||
if _take(extra, full_dest, seen_dests, taken):
|
||||
extras.append(extra)
|
||||
|
||||
return extras
|
||||
|
||||
@@ -517,6 +523,8 @@ def _collect_emulator_extras(
|
||||
)
|
||||
extras = []
|
||||
seen_dests: set[str] = set(seen)
|
||||
# Destination -> the extra that took it, to record later claimants on.
|
||||
taken: dict[str, dict] = {}
|
||||
for u in undeclared:
|
||||
if not u["in_repo"]:
|
||||
continue
|
||||
@@ -538,38 +546,18 @@ def _collect_emulator_extras(
|
||||
claimants[(u.get("emulator", ""), u.get("name", ""), u.get("path") or "")] = (
|
||||
sanitize_pack_path(dest)
|
||||
)
|
||||
if full_dest in seen_dests:
|
||||
extra = _report_extra(u, name, dest)
|
||||
claims = [_report_extra(c, name, dest) for c in u.get("other_claims", [])]
|
||||
# Where the builder meets each claim: the report lists the profiles in
|
||||
# order, archives after the loose files.
|
||||
for item in (extra, *claims):
|
||||
item["claim_order"] = (1, 1 if u.get("archive") else 0, item["source_profile"])
|
||||
if not _take(extra, full_dest, seen_dests, taken):
|
||||
for claim in claims:
|
||||
_another_claim(taken.get(full_dest), claim)
|
||||
continue
|
||||
seen_dests.add(full_dest)
|
||||
extra = {
|
||||
"name": name,
|
||||
"destination": dest,
|
||||
"required": u.get("required", False),
|
||||
"hle_fallback": u.get("hle_fallback", False),
|
||||
"source_emulator": u.get("emulator", ""),
|
||||
"source_profile": u.get("profile", ""),
|
||||
# Identity of the report entry this extra came from. The name
|
||||
# alone does not identify it: Dolphin declares three IPL.bin that
|
||||
# differ only by path, and collapsing them onto one key withdraws
|
||||
# the wrong regional variant from the report.
|
||||
"source_name": u.get("name", ""),
|
||||
"source_path": u.get("path") or "",
|
||||
"source_system": u.get("system"),
|
||||
"source_systems": u.get("systems", []),
|
||||
"region": u.get("region"),
|
||||
"variant_group": u.get("variant_group"),
|
||||
}
|
||||
# Keep every reproducible identity constraint carried by the source
|
||||
# profile. Some profiles expose CRC32/SHA-256 only through their
|
||||
# validation block; dropping those fields here would let a same-named
|
||||
# but different payload enter a generated pack.
|
||||
expected = u.get("expected") or {}
|
||||
for identity in PROFILE_IDENTITY_FIELDS:
|
||||
declared = u.get(identity)
|
||||
if declared in (None, "", []):
|
||||
declared = expected.get(identity)
|
||||
if declared not in (None, "", []):
|
||||
extra[identity] = declared
|
||||
for claim in claims:
|
||||
_another_claim(extra, claim)
|
||||
extras.append(extra)
|
||||
|
||||
# Second pass: find alternative destinations for files already in the pack.
|
||||
@@ -624,7 +612,7 @@ def _collect_emulator_extras(
|
||||
if dest == fname:
|
||||
continue # no alternative destination
|
||||
full_dest = f"{extras_prefix}/{dest}" if extras_prefix else dest
|
||||
if full_dest in seen_dests:
|
||||
if full_dest in seen_dests and full_dest not in taken:
|
||||
continue
|
||||
# Check file exists in repo or data dirs
|
||||
if not (
|
||||
@@ -633,7 +621,6 @@ def _collect_emulator_extras(
|
||||
or by_path_suffix.get(dest)
|
||||
):
|
||||
continue
|
||||
seen_dests.add(full_dest)
|
||||
extra = {
|
||||
"name": fname,
|
||||
"destination": dest,
|
||||
@@ -652,24 +639,89 @@ def _collect_emulator_extras(
|
||||
for identity in PROFILE_IDENTITY_FIELDS:
|
||||
if f.get(identity) not in (None, "", []):
|
||||
extra[identity] = f[identity]
|
||||
extras.append(extra)
|
||||
extra["claim_order"] = (2, 0, emu_name)
|
||||
if _take(extra, full_dest, seen_dests, taken):
|
||||
extras.append(extra)
|
||||
|
||||
extras.extend(
|
||||
_archive_prefix_extras(
|
||||
profiles, relevant, covered_names, seen_dests, extras_prefix,
|
||||
by_name,
|
||||
by_name, taken,
|
||||
)
|
||||
)
|
||||
|
||||
extras.extend(
|
||||
_agnostic_scan_extras(
|
||||
profiles, relevant, db, by_name, seen_dests, extras_prefix,
|
||||
standalone_set,
|
||||
standalone_set, taken,
|
||||
)
|
||||
)
|
||||
|
||||
return extras
|
||||
|
||||
def _report_extra(u: dict, name: str, dest: str) -> dict:
|
||||
"""The extra a cross-reference entry asks for at its destination."""
|
||||
extra = {
|
||||
"name": name,
|
||||
"destination": dest,
|
||||
"required": u.get("required", False),
|
||||
"hle_fallback": u.get("hle_fallback", False),
|
||||
"source_emulator": u.get("emulator", ""),
|
||||
"source_profile": u.get("profile", ""),
|
||||
# Identity of the report entry this extra came from. The name
|
||||
# alone does not identify it: Dolphin declares three IPL.bin that
|
||||
# differ only by path, and collapsing them onto one key withdraws
|
||||
# the wrong regional variant from the report.
|
||||
"source_name": u.get("name", ""),
|
||||
"source_path": u.get("path") or "",
|
||||
"source_system": u.get("system"),
|
||||
"source_systems": u.get("systems", []),
|
||||
"region": u.get("region"),
|
||||
"variant_group": u.get("variant_group"),
|
||||
}
|
||||
# Keep every reproducible identity constraint carried by the source
|
||||
# profile. Some profiles expose CRC32/SHA-256 only through their
|
||||
# validation block; dropping those fields here would let a same-named
|
||||
# but different payload enter a generated pack.
|
||||
expected = u.get("expected") or {}
|
||||
for identity in PROFILE_IDENTITY_FIELDS:
|
||||
declared = u.get(identity)
|
||||
if declared in (None, "", []):
|
||||
declared = expected.get(identity)
|
||||
if declared not in (None, "", []):
|
||||
extra[identity] = declared
|
||||
return extra
|
||||
|
||||
|
||||
def _take(
|
||||
extra: dict, full_dest: str, seen_dests: set[str], taken: dict[str, dict] | None
|
||||
) -> bool:
|
||||
"""Whether an extra takes its destination; a taken one records the claim."""
|
||||
if full_dest in seen_dests:
|
||||
if taken is not None:
|
||||
_another_claim(taken.get(full_dest), extra)
|
||||
return False
|
||||
seen_dests.add(full_dest)
|
||||
if taken is not None:
|
||||
taken[full_dest] = extra
|
||||
return True
|
||||
|
||||
|
||||
def _another_claim(extra: dict | None, claim: dict) -> None:
|
||||
"""Record a core that reads a destination another core's extra took.
|
||||
|
||||
The pack ships one file per destination, so the first claimant's extra
|
||||
stands; the others are kept on it so the manifest can name every core
|
||||
the file serves. A pack built for a target without the first core packs
|
||||
the next one's copy, and an installer narrowing by core must keep it.
|
||||
"""
|
||||
if extra is None or claim.get("source_profile") == extra.get("source_profile"):
|
||||
return
|
||||
claims = extra.setdefault("other_claims", [])
|
||||
claims.append(claim)
|
||||
claims.sort(key=lambda c: c["claim_order"])
|
||||
|
||||
|
||||
def _extra_system_ids(extra: dict) -> list[str]:
|
||||
"""Return the narrowest system ownership preserved on a core extra."""
|
||||
explicit = extra.get("source_system")
|
||||
|
||||
+35
-24
@@ -505,6 +505,10 @@ def find_undeclared_files(
|
||||
standalone_set = set(str(c) for c in config.get("standalone_cores", []))
|
||||
undeclared = []
|
||||
seen_files: set[tuple] = set()
|
||||
# Key -> the entry reported for it. A later profile reading the same
|
||||
# requirement is kept on that entry rather than counted again: a pack
|
||||
# built without the first profile still has to name the file.
|
||||
reported: dict[tuple, dict] = {}
|
||||
# Track archives: archive_name -> {in_repo, emulator, files: [...], ...}
|
||||
archive_entries: dict[tuple, dict] = {}
|
||||
|
||||
@@ -549,7 +553,11 @@ def find_undeclared_files(
|
||||
f.get("variant_group"),
|
||||
region_key,
|
||||
)
|
||||
if not fname or seen_key in seen_files:
|
||||
if not fname:
|
||||
continue
|
||||
first = reported.get(seen_key)
|
||||
duplicate = seen_key in seen_files
|
||||
if duplicate and (first is None or first["profile"] == emu_name):
|
||||
continue
|
||||
verdict = _candidate_verdict(
|
||||
f, fname, is_standalone, include_all, declared_names, dest
|
||||
@@ -644,29 +652,32 @@ def find_undeclared_files(
|
||||
in_repo = False
|
||||
|
||||
checks = _parse_validation(f.get("validation"))
|
||||
undeclared.append(
|
||||
{
|
||||
"profile": emu_name,
|
||||
"emulator": profile.get("emulator", emu_name),
|
||||
"systems": list(profile.get("systems", [])),
|
||||
"system": f.get("system"),
|
||||
"region": f.get("region"),
|
||||
"variant_group": f.get("variant_group"),
|
||||
"name": fname,
|
||||
"path": dest,
|
||||
"required": f.get("required", False),
|
||||
"hle_fallback": f.get("hle_fallback", False),
|
||||
"category": f.get("category", "bios"),
|
||||
"in_repo": in_repo,
|
||||
"note": f.get("note", ""),
|
||||
"checks": sorted(checks) if checks else [],
|
||||
"source_ref": f.get("source_ref"),
|
||||
"expected": _build_expected(f, checks),
|
||||
**_identity_of(f),
|
||||
"sha1": f.get("sha1"),
|
||||
"md5": f.get("md5"),
|
||||
}
|
||||
)
|
||||
entry = {
|
||||
"profile": emu_name,
|
||||
"emulator": profile.get("emulator", emu_name),
|
||||
"systems": list(profile.get("systems", [])),
|
||||
"system": f.get("system"),
|
||||
"region": f.get("region"),
|
||||
"variant_group": f.get("variant_group"),
|
||||
"name": fname,
|
||||
"path": dest,
|
||||
"required": f.get("required", False),
|
||||
"hle_fallback": f.get("hle_fallback", False),
|
||||
"category": f.get("category", "bios"),
|
||||
"in_repo": in_repo,
|
||||
"note": f.get("note", ""),
|
||||
"checks": sorted(checks) if checks else [],
|
||||
"source_ref": f.get("source_ref"),
|
||||
"expected": _build_expected(f, checks),
|
||||
**_identity_of(f),
|
||||
"sha1": f.get("sha1"),
|
||||
"md5": f.get("md5"),
|
||||
}
|
||||
if duplicate:
|
||||
first.setdefault("other_claims", []).append(entry)
|
||||
continue
|
||||
reported[seen_key] = entry
|
||||
undeclared.append(entry)
|
||||
|
||||
# Append grouped archive entries
|
||||
for entry in sorted(archive_entries.values(), key=lambda e: e["name"]):
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
"""The one-liner narrows to the cores a RetroArch already has.
|
||||
|
||||
RetroArch keeps its cores in libretro_directory, named the buildbot's way
|
||||
(`mednafen_psx_libretro.so`), while a file in the manifest names the profile
|
||||
that reads it (`beetle_psx`). The manifest maps one to the other and says
|
||||
which cores keep each system, the rule a pack built for a target follows.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import io
|
||||
import json
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO_ROOT))
|
||||
sys.path.insert(0, str(REPO_ROOT / "tests"))
|
||||
|
||||
import install # noqa: E402
|
||||
from test_install_selection import FakeRepository # noqa: E402
|
||||
|
||||
|
||||
class CoreDirectories(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
|
||||
self.addCleanup(lambda: __import__("shutil").rmtree(self.tmp, ignore_errors=True))
|
||||
|
||||
def test_the_config_detection_read_names_the_cores(self):
|
||||
cfg = self.tmp / "retroarch.cfg"
|
||||
cfg.write_text(
|
||||
f'system_directory = "{self.tmp / "sys"}"\n'
|
||||
f'libretro_directory = "{self.tmp / "my cores"}"\n'
|
||||
)
|
||||
system_dir = install._parse_retroarch_system_dir(cfg)
|
||||
self.assertEqual(
|
||||
install.core_dirs("retroarch", system_dir), [self.tmp / "my cores"]
|
||||
)
|
||||
|
||||
def test_a_portable_layout_defaults_to_cores_beside_the_config(self):
|
||||
(self.tmp / "retroarch.cfg").write_text('libretro_directory = "default"\n')
|
||||
self.assertEqual(
|
||||
install.core_dirs("retroarch", self.tmp / "system"), [self.tmp / "cores"]
|
||||
)
|
||||
(self.tmp / "retroarch.cfg").write_text('video_driver = "gl"\n')
|
||||
self.assertEqual(
|
||||
install.core_dirs("retroarch", self.tmp / "system"), [self.tmp / "cores"]
|
||||
)
|
||||
|
||||
def test_retropie_and_lakka_keep_their_own_folders(self):
|
||||
self.assertIn(
|
||||
Path("/opt/retropie/libretrocores"),
|
||||
install.core_dirs("retropie", self.tmp / "none"),
|
||||
)
|
||||
self.assertIn(Path("/storage/cores"), install.core_dirs("lakka", self.tmp / "none"))
|
||||
|
||||
def test_core_files_of_every_system_and_retropie_folders(self):
|
||||
cores = self.tmp / "cores"
|
||||
(cores / "lr-fceumm").mkdir(parents=True)
|
||||
for name in (
|
||||
"mednafen_psx_libretro.so", "snes9x_libretro.dll",
|
||||
"mgba_libretro.dylib", "genesis_plus_gx_libretro_android.so",
|
||||
"lr-fceumm/fceumm_libretro.so", "mgba_libretro.info", "readme.txt",
|
||||
):
|
||||
(cores / name).write_bytes(b"")
|
||||
self.assertEqual(
|
||||
install.installed_cores([cores, self.tmp / "missing"]),
|
||||
["fceumm", "genesis_plus_gx", "mednafen_psx", "mgba", "snes9x"],
|
||||
)
|
||||
|
||||
|
||||
MANIFEST = {
|
||||
"core_names": {"beetle_psx": ["mednafen_psx"]},
|
||||
"system_cores": {"sony-playstation": ["beetle_psx", "pcsx_rearmed"],
|
||||
"nintendo-gba": ["mgba"]},
|
||||
"files": [
|
||||
{"dest": "scph5501.bin", "cores": None, "systems": ["sony-playstation"]},
|
||||
{"dest": "gba_bios.bin", "cores": None, "systems": ["nintendo-gba"]},
|
||||
{"dest": "unlisted.bin", "cores": None, "systems": ["arcade"]},
|
||||
{"dest": "psx/extra.bin", "cores": ["beetle_psx"], "systems": ["sony-playstation"]},
|
||||
{"dest": "mgba/extra.bin", "cores": ["mgba"], "systems": ["nintendo-gba"]},
|
||||
],
|
||||
"omitted_files": [
|
||||
{"dest": "gone.bin", "cores": ["mgba"], "systems": ["nintendo-gba"]},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class NarrowToCores(unittest.TestCase):
|
||||
def test_a_buildbot_name_keeps_its_profile_and_its_systems(self):
|
||||
files, omitted = install.narrow_to_cores(MANIFEST, ["mednafen_psx"])
|
||||
self.assertEqual(
|
||||
[f["dest"] for f in files],
|
||||
["scph5501.bin", "unlisted.bin", "psx/extra.bin"],
|
||||
)
|
||||
self.assertEqual(omitted, [])
|
||||
|
||||
def test_an_older_list_keeps_the_platform_files(self):
|
||||
older = {k: v for k, v in MANIFEST.items() if k in ("files", "omitted_files")}
|
||||
files, _ = install.narrow_to_cores(older, ["mgba"])
|
||||
self.assertEqual(
|
||||
[f["dest"] for f in files],
|
||||
["scph5501.bin", "gba_bios.bin", "unlisted.bin", "mgba/extra.bin"],
|
||||
)
|
||||
|
||||
|
||||
class TheBuildersOrder(unittest.TestCase):
|
||||
"""RetroPie reads megaplay.zip as advancemame (copy A), mame (copy B),
|
||||
then mame2003 (copy A). Without advancemame the builder ships mame's
|
||||
copy; merging mame2003 into the first copy gave it A."""
|
||||
|
||||
ENTRY = {
|
||||
"dest": "megaplay.zip", "sha1": "a" * 40, "size": 1,
|
||||
"repo_path": "bios/a.zip", "cores": ["advancemame"], "systems": ["arcade"],
|
||||
"alternatives": [
|
||||
{"sha1": "b" * 40, "sha256": "", "size": 2, "repo_path": "bios/b.zip",
|
||||
"cores": ["mame"]},
|
||||
{"sha1": "a" * 40, "sha256": "", "size": 1, "repo_path": "bios/a.zip",
|
||||
"cores": ["mame2003"]},
|
||||
],
|
||||
}
|
||||
|
||||
def test_the_first_chosen_claim_wins(self):
|
||||
manifest = {"files": [self.ENTRY], "omitted_files": []}
|
||||
for names, sha in ((["mame", "mame2003"], "b"), (["mame2003"], "a"),
|
||||
(["advancemame", "mame"], "a")):
|
||||
with self.subTest(names=names):
|
||||
files, _ = install.narrow_to_cores(manifest, names)
|
||||
self.assertEqual(files[0]["sha1"], sha * 40)
|
||||
self.assertNotIn("alternatives", files[0] if sha == "b" else {})
|
||||
|
||||
def test_the_parity_covers_a_core_set(self):
|
||||
"""A real build for mame and mame2003 on RetroPie, the shape
|
||||
--installed-cores produces there."""
|
||||
db_path = REPO_ROOT / "database.json"
|
||||
if not db_path.exists():
|
||||
self.skipTest("database.json is not built")
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
import generate_pack as gp
|
||||
from common import load_emulator_profiles
|
||||
|
||||
db = json.loads(db_path.read_text(encoding="utf-8"))
|
||||
profiles = load_emulator_profiles(str(REPO_ROOT / "emulators"))
|
||||
|
||||
def manifest(cores):
|
||||
return gp.generate_manifest(
|
||||
"retropie", str(REPO_ROOT / "platforms"), db, str(REPO_ROOT / "bios"),
|
||||
str(REPO_ROOT / "platforms" / "_registry.yml"),
|
||||
emulators_dir=str(REPO_ROOT / "emulators"), emu_profiles=profiles,
|
||||
target_cores=cores, offline=True,
|
||||
)
|
||||
|
||||
names = ["mame", "mame2003"]
|
||||
files, _ = install.narrow_to_cores(manifest(None), names)
|
||||
built = manifest(set(names))
|
||||
self.assertEqual(
|
||||
{(f["dest"], f["sha1"]) for f in files},
|
||||
{(f["dest"], f["sha1"]) for f in built["files"]},
|
||||
)
|
||||
|
||||
|
||||
class TheBoundaryBoundsAlternatives(unittest.TestCase):
|
||||
def _manifest(self, alternative: dict) -> dict:
|
||||
entry = {"dest": "a.bin", "sha1": "a" * 40, "size": 1,
|
||||
"repo_path": "bios/a.bin", "cores": ["x"],
|
||||
"alternatives": [alternative]}
|
||||
return {"manifest_version": 2, "platform": "retroarch", "files": [entry]}
|
||||
|
||||
def test_an_alternative_carries_only_a_copy(self):
|
||||
good = {"sha1": "b" * 40, "size": 1, "repo_path": "bios/b.bin", "cores": ["y"]}
|
||||
install._validate_manifest(self._manifest(good), "retroarch")
|
||||
for extra in ({"dest": "../../evil"}, {"regions": 5}, {"cores": []},
|
||||
{"repo_path": "../x"}):
|
||||
with self.subTest(extra=extra), self.assertRaises(ValueError):
|
||||
install._validate_manifest(self._manifest({**good, **extra}), "retroarch")
|
||||
|
||||
def test_the_cap_counts_the_largest_copy(self):
|
||||
big = {"sha1": "b" * 40, "size": install.MAX_DOWNLOAD_SIZE,
|
||||
"repo_path": "bios/b.bin", "cores": ["y"]}
|
||||
manifest = self._manifest(big)
|
||||
manifest["files"] = [
|
||||
{**manifest["files"][0], "dest": f"f{i}.bin"} for i in range(70)
|
||||
]
|
||||
with self.assertRaises(ValueError):
|
||||
install._validate_manifest(manifest, "retroarch")
|
||||
|
||||
|
||||
class RefusedBeforeAnyInstall(unittest.TestCase):
|
||||
def test_a_platform_shipping_its_cores_stops_the_run_first(self):
|
||||
tmp = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
|
||||
self.addCleanup(shutil.rmtree, tmp, True)
|
||||
detected = [("retroarch", tmp / "system"), ("batocera", tmp / "bios")]
|
||||
(tmp / "retroarch.cfg").write_text("")
|
||||
(tmp / "cores").mkdir()
|
||||
(tmp / "cores" / "mgba_libretro.so").write_bytes(b"")
|
||||
with mock.patch.object(install, "detect_os", return_value="linux"), \
|
||||
mock.patch.object(install, "detect_frontends", return_value=[]), \
|
||||
mock.patch.object(install, "detect_platforms", return_value=detected), \
|
||||
mock.patch.object(install, "fetch_manifest") as fetch, \
|
||||
mock.patch.object(sys, "argv", ["install.py", "--installed-cores", "--no-input"]), \
|
||||
contextlib.redirect_stdout(io.StringIO()), \
|
||||
contextlib.redirect_stderr(io.StringIO()) as err:
|
||||
with self.assertRaises(SystemExit) as stop:
|
||||
install.main()
|
||||
self.assertEqual(stop.exception.code, 1)
|
||||
self.assertIn("batocera ships its cores", err.getvalue())
|
||||
fetch.assert_not_called()
|
||||
|
||||
|
||||
class _CoreRepository(FakeRepository):
|
||||
"""The fake repository with the two maps and a portable RetroArch."""
|
||||
|
||||
def __enter__(self):
|
||||
super().__enter__()
|
||||
path = self.root / "install" / "retroarch.json"
|
||||
manifest = json.loads(path.read_text())
|
||||
manifest["core_names"] = {"pcsx_rearmed": ["pcsx_rearmed_neon"]}
|
||||
manifest["system_cores"] = {
|
||||
"sony-playstation": ["pcsx_rearmed"], "nintendo-gba": ["mgba"],
|
||||
}
|
||||
path.write_text(json.dumps(manifest))
|
||||
(self.root / "retroarch.cfg").write_text('libretro_directory = ":/cores"\n')
|
||||
(self.root / "cores").mkdir()
|
||||
return self
|
||||
|
||||
def install_core(self, name: str) -> None:
|
||||
(self.root / "cores" / f"{name}_libretro.so").write_bytes(b"")
|
||||
|
||||
|
||||
class TheOneLinerReadsInstalledCores(unittest.TestCase):
|
||||
def test_only_what_the_installed_cores_read(self):
|
||||
with _CoreRepository() as repo:
|
||||
repo.install_core("pcsx_rearmed_neon")
|
||||
proc = repo.run("--installed-cores")
|
||||
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
|
||||
self.assertIn("1 core installed", proc.stdout)
|
||||
self.assertEqual(
|
||||
repo.installed(), ["pcsx/extra.bin", "scph5500.bin", "scph5501.bin"]
|
||||
)
|
||||
|
||||
def test_no_core_found_is_an_error_not_everything(self):
|
||||
with _CoreRepository() as repo:
|
||||
proc = repo.run("--installed-cores")
|
||||
self.assertEqual(proc.returncode, 1)
|
||||
self.assertIn("no installed core found", proc.stderr)
|
||||
self.assertEqual(repo.installed(), [])
|
||||
|
||||
def test_an_older_file_list_is_refused(self):
|
||||
with FakeRepository() as repo:
|
||||
(repo.root / "retroarch.cfg").write_text("")
|
||||
(repo.root / "cores").mkdir()
|
||||
(repo.root / "cores" / "mgba_libretro.so").write_bytes(b"")
|
||||
proc = repo.run("--installed-cores")
|
||||
self.assertEqual(proc.returncode, 1)
|
||||
self.assertIn("needs a newer one", proc.stderr)
|
||||
|
||||
def test_a_platform_that_ships_its_cores_refuses_the_flag(self):
|
||||
with _CoreRepository() as repo:
|
||||
batocera = repo.root / "install" / "batocera.json"
|
||||
manifest = json.loads((repo.root / "install" / "retroarch.json").read_text())
|
||||
manifest["platform"] = "batocera"
|
||||
batocera.write_text(json.dumps(manifest))
|
||||
command = repo.command("--installed-cores")
|
||||
command[command.index("retroarch")] = "batocera"
|
||||
proc = subprocess.run(
|
||||
command, env=repo.env(), capture_output=True, text=True,
|
||||
timeout=60, check=False, stdin=subprocess.DEVNULL,
|
||||
)
|
||||
self.assertEqual(proc.returncode, 1)
|
||||
self.assertIn("ships its cores with the system", proc.stderr)
|
||||
|
||||
|
||||
@unittest.skipUnless(shutil.which("script"), "script(1) provides the terminal")
|
||||
def test_the_prompt_offers_the_installed_cores(self):
|
||||
with _CoreRepository() as repo:
|
||||
repo.install_core("mgba")
|
||||
command = " ".join(shlex.quote(part) for part in repo.command())
|
||||
proc = subprocess.run(
|
||||
["script", "-qec", command, "/dev/null"],
|
||||
input="i\n", env=repo.env(), capture_output=True,
|
||||
text=True, timeout=60, check=False,
|
||||
)
|
||||
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
|
||||
self.assertIn("i) only what the installed cores read: 2 files", proc.stdout)
|
||||
self.assertIn("--platform retroarch --installed-cores", proc.stdout)
|
||||
self.assertEqual(repo.installed(), ["gba_bios.bin", "mgba/extra.bin"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -257,6 +257,19 @@ class TheBoundaryChecksTheNewFields(unittest.TestCase):
|
||||
with self.subTest(field=bad), self.assertRaises(ValueError):
|
||||
install._validate_manifest(self._manifest(**bad), "retroarch")
|
||||
|
||||
def test_the_core_maps(self):
|
||||
good = self._manifest()
|
||||
good.update(core_names={"beetle_psx": ["mednafen_psx"]},
|
||||
system_cores={"sony-playstation": ["beetle_psx"]})
|
||||
install._validate_manifest(good, "retroarch")
|
||||
for field, bad in (("core_names", ["x"]), ("core_names", {"a": "b"}),
|
||||
("system_cores", {"s": None}), ("system_cores", {"": ["a"]}),
|
||||
("system_cores", {"s": [1]})):
|
||||
manifest = self._manifest()
|
||||
manifest[field] = bad
|
||||
with self.subTest(field=field, value=bad), self.assertRaises(ValueError):
|
||||
install._validate_manifest(manifest, "retroarch")
|
||||
|
||||
|
||||
class _QuietHandler(http.server.SimpleHTTPRequestHandler):
|
||||
def log_message(self, format, *args): # noqa: A002 - stdlib signature
|
||||
@@ -555,5 +568,67 @@ class InstallMatchesASystemPack(unittest.TestCase):
|
||||
)
|
||||
|
||||
|
||||
class InstallMatchesATargetManifest(unittest.TestCase):
|
||||
"""`install.py --target T` against the manifest the builder writes for T.
|
||||
|
||||
A target list names cores the way the buildbot does (`mednafen_psx`)
|
||||
while a file names the profile that reads it (`beetle_psx`), and the
|
||||
builder drops a platform system no available core reads. The installer
|
||||
did neither: on RetroArch desktop it kept every system and lost the
|
||||
extras of five cores. A destination two cores read was also filed under
|
||||
the first alone, so a target without that core lost it, or got the
|
||||
first core's copy where the other core wants different bytes
|
||||
(megaplay.zip for mame and mame2003 on Switch).
|
||||
"""
|
||||
|
||||
CASES = (
|
||||
("retroarch", "desktop"),
|
||||
("retroarch", "switch"),
|
||||
("batocera", "x86_64"),
|
||||
("batocera", "bcm2711"),
|
||||
("retropie", "rpi4"),
|
||||
("emudeck", "steamos"),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
db_path = REPO_ROOT / "database.json"
|
||||
if not db_path.exists():
|
||||
raise unittest.SkipTest("database.json is not built")
|
||||
import generate_pack as gp
|
||||
from common import load_emulator_profiles, load_target_config
|
||||
|
||||
cls.gp = gp
|
||||
cls.load_target_config = staticmethod(load_target_config)
|
||||
cls.db = json.loads(db_path.read_text(encoding="utf-8"))
|
||||
cls.profiles = load_emulator_profiles(str(REPO_ROOT / "emulators"))
|
||||
|
||||
def _manifest(self, platform, target_cores=None, target=None):
|
||||
return self.gp.generate_manifest(
|
||||
platform, str(REPO_ROOT / "platforms"), self.db,
|
||||
str(REPO_ROOT / "bios"), str(REPO_ROOT / "platforms" / "_registry.yml"),
|
||||
emulators_dir=str(REPO_ROOT / "emulators"), emu_profiles=self.profiles,
|
||||
target_cores=target_cores, target_name=target, offline=True,
|
||||
)
|
||||
|
||||
def test_each_target(self):
|
||||
for platform, target in self.CASES:
|
||||
with self.subTest(platform=platform, target=target):
|
||||
cores = sorted(self.load_target_config(
|
||||
platform, target, str(REPO_ROOT / "platforms")
|
||||
))
|
||||
full = self._manifest(platform)
|
||||
built = self._manifest(platform, set(cores), target)
|
||||
files, omitted = install.narrow_to_cores(full, cores)
|
||||
self.assertEqual(
|
||||
{(f["dest"], f["sha1"]) for f in files},
|
||||
{(f["dest"], f["sha1"]) for f in built["files"]},
|
||||
)
|
||||
self.assertEqual(
|
||||
{o["dest"] for o in omitted},
|
||||
{o["dest"] for o in built["omitted_files"]},
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+34
-2
@@ -118,10 +118,17 @@ after detection, it asks:
|
||||
```
|
||||
Retroarch: 5861 files, 6.0 GB.
|
||||
Enter) install everything
|
||||
i) only what the installed cores read: 102 files, 44.5 MB
|
||||
c) choose systems, cores and regions
|
||||
q) quit
|
||||
```
|
||||
|
||||
`i` appears for RetroArch, Lakka and RetroPie when cores are found installed;
|
||||
the line above is a RetroArch with Beetle PSX HW, Snes9x, mGBA, Genesis Plus GX
|
||||
and FCEUmm.
|
||||
It keeps what those cores read and drops the systems none of them emulates,
|
||||
the rule `--installed-cores` applies.
|
||||
|
||||
`c` walks through three numbered lists, each answered with numbers such as
|
||||
`1,4,7-9`, or Enter to keep everything on that axis.
|
||||
|
||||
@@ -149,6 +156,30 @@ python install.py --platform retroarch --system sony-playstation --core pcsx_rea
|
||||
python install.py --platform recalbox --region us,eu,jp
|
||||
```
|
||||
|
||||
### Installed cores
|
||||
|
||||
`--installed-cores` reads the cores a RetroArch already has and installs what
|
||||
they need. The cores are the `<name>_libretro.so` (`.dll`, `.dylib`,
|
||||
`_libretro_android.so`) files in the `libretro_directory` of the
|
||||
`retroarch.cfg` detection found, `cores/` beside RetroArch when the setting is
|
||||
unset, a `retroarch.cfg` next to the system directory for a portable install
|
||||
given with `--dest`, and the folders RetroPie (`/opt/retropie/libretrocores`)
|
||||
and Lakka (`/usr/lib/libretro`, `/storage/cores`) install into. A core file
|
||||
is named the buildbot's way (`mednafen_psx`); the manifest maps each name to
|
||||
the profile its files are filed under (`beetle_psx`).
|
||||
|
||||
What stays is what a pack built for exactly those cores carries, the rule
|
||||
`--target` follows too: a platform system stays when an installed core
|
||||
emulates it or reads its files, or when no core of the platform does; a
|
||||
file a core loads stays when that core is installed. Where two cores read
|
||||
one path and want different bytes, the copy of the first installed core in
|
||||
the builder's order is the one installed.
|
||||
|
||||
Batocera, Recalbox and the other distributions ship their cores with the
|
||||
system, so the option is refused there, as it is when no core is found or
|
||||
when the file list predates the mapping: carrying on would install
|
||||
everything while the option asked for less.
|
||||
|
||||
A name the platform does not have is refused with the list of the ones it
|
||||
has, before anything is downloaded: carrying on would install something other
|
||||
than what was asked. A system name that itself contains commas, as Recalbox's
|
||||
@@ -173,7 +204,8 @@ what the options name, everything by default.
|
||||
|--------|--------|
|
||||
| `--platform NAME` | Install for this platform instead of the detected one. Unknown names are refused with the available list |
|
||||
| `--dest PATH` | Destination directory, overriding detection. With `--dest` alone the file list is RetroArch's |
|
||||
| `--target NAME` | Keep only the files the cores of that hardware target need. An unknown target is refused rather than ignored, since carrying on would install everything |
|
||||
| `--target NAME` | Keep only the files the cores of that hardware target need, and the systems they emulate. An unknown target is refused rather than ignored, since carrying on would install everything |
|
||||
| `--installed-cores` | Keep only what the cores already installed read (RetroArch, Lakka, RetroPie); see [Installed cores](#installed-cores) |
|
||||
| `--system NAMES` | Install only these systems. Comma-separated or repeated; `--list-systems` prints the names, `other` included |
|
||||
| `--core NAMES` | Keep the platform's own list and only the extra files these cores load; `--list-cores` prints the names |
|
||||
| `--region LIST` | Preferred regions, best first (`us,eu,jp`): keep one BIOS per region where a system has several. Accepts the names and aliases of `generate_pack.py --region` |
|
||||
@@ -379,7 +411,7 @@ followed.
|
||||
## Exit status
|
||||
|
||||
`0` on success, `1` on failure: an unusable base URL, an unknown platform or
|
||||
target, a manifest that fails validation, a fetch that never succeeded, or no
|
||||
target, `--installed-cores` with no core found, a manifest that fails validation, a fetch that never succeeded, or no
|
||||
platform to install for in a non-interactive run.
|
||||
|
||||
Failures that are per-file, such as a download exhausting its three attempts,
|
||||
|
||||
Reference in new issue
Block a user