diff --git a/platforms/_registry.yml b/platforms/_registry.yml index e3d4c9544..5752f6f29 100644 --- a/platforms/_registry.yml +++ b/platforms/_registry.yml @@ -1011,7 +1011,9 @@ platforms: # EmuHawk finds firmware by SHA1 against FirmwareDatabase.FirmwareRecords # and its cores are its own builds (CoreNames.cs): no libretro profile # describes what it reads, so the database alone decides, as MiSTer's - # BiosDB does. + # BiosDB does. emulators/bizhawk.yml also documents the options the scan + # never takes, so it is not listed here, and --region, --one-per-slot + # and --no-game-data are refused for this platform. cores: [] target_scraper: null target_source: null diff --git a/scripts/common.py b/scripts/common.py index d0f568481..62be6e5cf 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -2761,6 +2761,29 @@ def preferred_profile(index: dict[str, set[str]], name: str) -> str | None: return name if name in claimants else sorted(claimants)[0] +def profile_blind_platforms( + platforms: list[str], + platforms_dir: str, + profiles: dict[str, dict], + target_cores: dict[str, set[str] | None] | None = None, +) -> list[str]: + """Platforms that run no core a profile describes. + + Regions, search order and game data are read from the profiles of the + cores a platform runs. MiSTer and BizHawk declare `cores: []`: a + --region, --one-per-slot or --no-game-data pack of theirs withdrew + nothing while its name and README said it had. + """ + return [ + name for name in platforms + if not resolve_platform_cores( + load_platform_config(name, platforms_dir), + profiles, + target_cores=(target_cores or {}).get(name), + ) + ] + + def resolve_platform_cores( config: dict, profiles: dict[str, dict], diff --git a/scripts/generate_pack.py b/scripts/generate_pack.py index 36e15ba85..7634b7cb6 100644 --- a/scripts/generate_pack.py +++ b/scripts/generate_pack.py @@ -75,6 +75,7 @@ from common import ( load_database, load_emulator_profiles, load_platform_config, + profile_blind_platforms, MANUFACTURER_PREFIXES, require_yaml, ProfileSelectionError, @@ -3074,6 +3075,42 @@ def _pack_label(source: str, required_only: bool) -> str: return f"[source={source}, required]" if required_only else f"[source={source}]" +def _without_profile_blind( + args: argparse.Namespace, + platforms: list[str], + emu_profiles: dict, + target_cores_cache: dict[str, set[str] | None], +) -> list[str]: + """Refuse a narrowing read from profiles on a platform that runs none. + + Named, the platform is refused. Under --all it is left out with a + notice, as an empty truth pack is under --all-variants. + """ + flags = [ + flag for flag, given in ( + ("--region", getattr(args, "regions", None)), + ("--one-per-slot", getattr(args, "one_per_slot", False)), + ("--no-game-data", getattr(args, "no_game_data", False)), + ) if given + ] + if not flags: + return platforms + blind = profile_blind_platforms( + platforms, args.platforms_dir, emu_profiles, + target_cores_cache if args.target else None, + ) + for name in blind: + message = ( + f"{name} runs no core a profile describes, and {', '.join(flags)} " + "reads the profiles" + ) + if not args.all: + print(f"ERROR: {message}: drop {', '.join(flags)}", file=sys.stderr) + sys.exit(1) + print(f" {message}: left out") + return [name for name in platforms if name not in blind] + + def _truth_layer_is_empty( platform: str, platforms_dir: str, emu_profiles: dict, target_cores: set[str] | None ) -> bool: @@ -3785,6 +3822,9 @@ def _main(held: contextlib.ExitStack) -> None: platforms = _platforms_with_systems( args, platforms, system_filter, emu_profiles, target_cores_cache ) + platforms = _without_profile_blind( + args, platforms, emu_profiles, target_cores_cache + ) data_registry = load_data_dir_registry(args.platforms_dir) # The output directory is claimed before the data directory refresh: a diff --git a/scripts/verify.py b/scripts/verify.py index bd6c7069a..01c29717b 100644 --- a/scripts/verify.py +++ b/scripts/verify.py @@ -65,6 +65,7 @@ from common import ( load_database, load_emulator_profiles, load_platform_config, + profile_blind_platforms, resolve_system_arg, print_platform_listing, select_systems, @@ -3017,6 +3018,30 @@ def _main(held: contextlib.ExitStack) -> None: print(f"ERROR: {reason}", file=sys.stderr) sys.exit(1) + # generate_pack refuses these on a platform whose cores no profile + # describes, and the report answers the same request the same way. + flags = [ + flag for flag, given in ( + ("--region", requested_regions), + ("--no-game-data", getattr(args, "no_game_data", False)), + ) if given + ] + if flags: + blind = profile_blind_platforms( + platforms, args.platforms_dir, emu_profiles, + target_cores_cache if args.target else None, + ) + for name in blind: + message = ( + f"{name} runs no core a profile describes, and " + f"{', '.join(flags)} reads the profiles" + ) + if not args.all: + print(f"ERROR: {message}: drop {', '.join(flags)}", file=sys.stderr) + sys.exit(1) + print(f" {message}: left out") + platforms = [name for name in platforms if name not in blind] + # Group identical platforms (same function as generate_pack) groups = group_identical_platforms( platforms, args.platforms_dir, target_cores_cache if args.target else None diff --git a/tests/test_e2e.py b/tests/test_e2e.py index 894b43a44..b05a93682 100644 --- a/tests/test_e2e.py +++ b/tests/test_e2e.py @@ -6374,6 +6374,33 @@ struct BurnDriver BurnDrvneogeo = { [f for f in os.listdir(out) if f.endswith(".zip")] if os.path.isdir(out) else [] ) + def test_a_platform_without_cores_refuses_profile_narrowings(self): + """MiSTer and BizHawk declare cores: []. --region, --one-per-slot + and --no-game-data read the profiles, so their packs withdrew + nothing under a name and README that said otherwise.""" + import subprocess + + repo = os.path.join(os.path.dirname(__file__), "..") + out = os.path.join(self.root, "blind") + runs = [ + ["scripts/generate_pack.py", "--platform", "misterfpga", "--region", "us"], + ["scripts/generate_pack.py", "--platform", "misterfpga", "--one-per-slot"], + ["scripts/generate_pack.py", "--platform", "misterfpga", "--no-game-data"], + ["scripts/generate_pack.py", "--platform", "bizhawk", "--manifest", + "--region", "us"], + ["scripts/verify.py", "--platform", "bizhawk", "--region", "us"], + ] + for argv in runs: + extra = ["--offline", "--output-dir", out] if "generate_pack" in argv[0] else [] + with self.subTest(argv=argv): + proc = subprocess.run( + [sys.executable, *argv, *extra], + capture_output=True, text=True, cwd=repo, timeout=300, + ) + self.assertEqual(proc.returncode, 1, proc.stdout + proc.stderr) + self.assertIn("runs no core a profile describes", proc.stderr) + self.assertFalse(os.listdir(out) if os.path.isdir(out) else []) + def test_a_full_pack_is_not_announced_as_narrowed(self): from generate_pack import _build_readme, _narrowings diff --git a/wiki/advanced-usage.md b/wiki/advanced-usage.md index aff9b10b6..c4210b3b5 100644 --- a/wiki/advanced-usage.md +++ b/wiki/advanced-usage.md @@ -140,6 +140,11 @@ Three rules keep a filtered pack usable: - **Untagged files always survive.** Most BIOS are not regional and carry no `region:` at all. +Regions, the search order `--one-per-slot` reads and the game data +`--no-game-data` cuts all come from the profiles of the cores a platform runs. +MiSTer and BizHawk declare no cores, their own databases decide, so the three +flags are refused for them. Under `--all` they are left out with a notice. + What this does and does not do: it shrinks the pack. It does not change how cores pick a BIOS. Most cores already select per region from fixed filename lists driven by the game's region (PicoDrive in `libretro.c`, Genesis Plus GX in `loadrom.c`),