diff --git a/emulators/00_example.yml b/emulators/00_example.yml index 6f43c3a15..c30955a9e 100644 --- a/emulators/00_example.yml +++ b/emulators/00_example.yml @@ -1,5 +1,6 @@ emulator: "00_example" -type: test +type: libretro +role: test core_classification: pure_libretro source: "https://github.com/libretro/libretro-core-info" profiled_date: "2026-03-22" diff --git a/emulators/2048.yml b/emulators/2048.yml index b979db5af..88e74354f 100644 --- a/emulators/2048.yml +++ b/emulators/2048.yml @@ -1,5 +1,6 @@ emulator: "2048" -type: game +type: libretro +role: game core_classification: pure_libretro source: "https://github.com/libretro/libretro-2048" upstream: "https://github.com/gabrielecirulli/2048" diff --git a/emulators/gong.yml b/emulators/gong.yml index 066fe86e7..e77a72916 100644 --- a/emulators/gong.yml +++ b/emulators/gong.yml @@ -1,5 +1,6 @@ emulator: Gong -type: game +type: libretro +role: game core_classification: pure_libretro source: "https://github.com/libretro/gong" upstream: "https://danzaidan.itch.io/" diff --git a/emulators/radio.yml b/emulators/radio.yml index f677ed092..45e1af14c 100644 --- a/emulators/radio.yml +++ b/emulators/radio.yml @@ -1,5 +1,6 @@ emulator: "Radio" -type: utility +type: libretro +role: utility core_classification: pure_libretro source: "https://git.libretro.com/libretro/libretro-radio" upstream: "https://github.com/fpscan/libretro-radio" diff --git a/emulators/test.yml b/emulators/test.yml index 7aa106683..44a768af3 100644 --- a/emulators/test.yml +++ b/emulators/test.yml @@ -1,5 +1,6 @@ emulator: test -type: test +type: libretro +role: test core_classification: pure_libretro source: "https://github.com/libretro/libretro-samples" upstream: "https://github.com/libretro/libretro-samples" diff --git a/emulators/testgl.yml b/emulators/testgl.yml index 2c0d9a58c..b775552ee 100644 --- a/emulators/testgl.yml +++ b/emulators/testgl.yml @@ -1,5 +1,6 @@ emulator: testgl -type: test +type: libretro +role: test core_classification: pure_libretro source: "https://github.com/libretro/libretro-samples" upstream: "https://github.com/libretro/libretro-samples" diff --git a/emulators/testvulkan.yml b/emulators/testvulkan.yml index bdfa9d66c..9fc34955d 100644 --- a/emulators/testvulkan.yml +++ b/emulators/testvulkan.yml @@ -1,5 +1,6 @@ emulator: testvulkan -type: test +type: libretro +role: test core_classification: pure_libretro source: "https://github.com/libretro/libretro-samples" upstream: "https://github.com/libretro/libretro-samples" diff --git a/emulators/video_processor.yml b/emulators/video_processor.yml index 9a3e1f187..873c30fc5 100644 --- a/emulators/video_processor.yml +++ b/emulators/video_processor.yml @@ -1,5 +1,6 @@ emulator: "V4L2" -type: utility +type: libretro +role: utility core_classification: pure_libretro source: "https://github.com/libretro/libretro-video-processor" source_mirror: "https://git.libretro.com/libretro/libretro-video-processor" diff --git a/emulators/vidtest.yml b/emulators/vidtest.yml index d690e1e5d..654937f60 100644 --- a/emulators/vidtest.yml +++ b/emulators/vidtest.yml @@ -1,5 +1,6 @@ emulator: vidtest -type: test +type: libretro +role: test core_classification: pure_libretro source: "https://github.com/schellingb/vidtest_libretro" profiled_date: "2026-03-25" diff --git a/schemas/emulator.schema.json b/schemas/emulator.schema.json index b05d2d74f..7d8b1a1e6 100644 --- a/schemas/emulator.schema.json +++ b/schemas/emulator.schema.json @@ -121,6 +121,14 @@ "standalone + libretro", "alias", "launcher", + "game", + "utility" + ] + }, + "role": { + "description": "What a libretro core is for when it emulates no machine: a game of its own (2048), a utility (radio, video_processor), or a test core. Descriptive: the core stays type libretro, so every reader of all_libretro reaches it, and the site and coverage leave test cores out by this field.", + "type": "string", + "enum": [ "game", "utility", "test" diff --git a/scripts/common.py b/scripts/common.py index e756ffbeb..2529dfed9 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -2209,7 +2209,7 @@ def profiles_for_systems( wanted = {_norm_system_id(sid) for sid in system_ids} matching = [] for name, profile in sorted(profiles.items()): - if profile.get("type") in ("launcher", "alias", "test"): + if profile.get("type") in ("launcher", "alias") or profile.get("role") == "test": continue if standalone and "standalone" not in profile.get("type", "libretro"): continue @@ -2263,7 +2263,7 @@ def select_emulator_profiles( # and the spelling hints sent pcfx to two PlayStation cores. claimants = sorted( key for key in upstream_profile_index(all_profiles).get(name, ()) - if all_profiles[key].get("type") not in ("launcher", "test") + if all_profiles[key].get("type") != "launcher" and all_profiles[key].get("role") != "test" ) if len(claimants) == 1: name = claimants[0] @@ -2276,7 +2276,7 @@ def select_emulator_profiles( available = sorted( key for key, value in all_profiles.items() - if value.get("type") not in ("alias", "test") + if value.get("type") != "alias" and value.get("role") != "test" ) raise ProfileSelectionError( f"emulator '{name}' not found\n" @@ -2421,7 +2421,7 @@ def _normalize_hash_fields(node: object) -> None: def unique_emulator_profiles(profiles: dict[str, dict]) -> dict[str, dict]: """Profiles counting as distinct emulators (aliases and tests excluded).""" return { - k: v for k, v in profiles.items() if v.get("type") not in ("alias", "test") + k: v for k, v in profiles.items() if v.get("type") != "alias" and v.get("role") != "test" } @@ -3993,7 +3993,7 @@ def list_emulator_profiles(emulators_dir: str, skip_aliases: bool = True) -> Non profiles = load_emulator_profiles(emulators_dir, skip_aliases=False) for name in sorted(profiles): p = profiles[name] - if p.get("type") in ("alias", "test"): + if p.get("type") == "alias" or p.get("role") == "test": continue display = p.get("emulator", name) ptype = p.get("type", "libretro") @@ -4014,7 +4014,7 @@ def system_index(profiles: dict) -> dict[str, tuple[str, set[str]]]: """ index: dict[str, tuple[str, set[str]]] = {} for name, p in sorted(profiles.items()): - if p.get("type") in ("alias", "test", "launcher"): + if p.get("type") in ("alias", "launcher") or p.get("role") == "test": continue for sys_id in p.get("systems", []): key = _norm_system_id(sys_id) diff --git a/scripts/generate_site.py b/scripts/generate_site.py index 2a96b436d..936a3a3ea 100644 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -1174,7 +1174,7 @@ def build_emulator_gap_report( unique_profiles = { k: v for k, v in profiles.items() - if v.get("type") not in ("alias", "test") + if v.get("type") != "alias" and v.get("role") != "test" } from common import merged_standalone_set, platform_declarations from validation import case_folded_slots @@ -1306,7 +1306,7 @@ def _cross_reference_export_rows(coverages: dict, profiles: dict) -> list[dict]: unique = { key: value for key, value in profiles.items() - if value.get("type") not in ("alias", "test") + if value.get("type") != "alias" and value.get("role") != "test" } rows: list[dict] = [] for platform_id, coverage in sorted(coverages.items()): @@ -1922,7 +1922,7 @@ def system_index_items(db: dict, coverages: dict, profiles: dict) -> list[dict]: if sha1: held.setdefault(str(detail.get("system", "")), set()).add(sha1) for profile in profiles.values(): - if profile.get("type") in ("alias", "test"): + if profile.get("type") == "alias" or profile.get("role") == "test": continue own = [str(s) for s in profile.get("systems") or []] for entry in profile.get("files") or []: @@ -1973,7 +1973,7 @@ def _first_file_place( return parts[1], _site_path(f"systems/{page}.md#sha1-{sha1[:12]}") for key in sorted(profiles): profile = profiles[key] - if profile.get("type") not in ("alias", "test") and sys_id in (profile.get("systems") or []): + if profile.get("type") != "alias" and profile.get("role") != "test" and sys_id in (profile.get("systems") or []): return "", _site_path(f"emulators/{key}.md") return "", None @@ -1988,7 +1988,7 @@ def core_index_items(profiles: dict) -> list[dict]: """ from common import preferred_profile, upstream_profile_index - published = {name for name, p in profiles.items() if p.get("type") not in ("alias", "test")} + published = {name for name, p in profiles.items() if p.get("type") != "alias" and p.get("role") != "test"} index = upstream_profile_index(profiles) for name, profile in profiles.items(): target = str(profile.get("alias_of") or "") @@ -3602,7 +3602,7 @@ def _emulator_named_link(key: str, prefix: str = "") -> str: profile with no page (an alias, a test) is named only.""" profile = _profiles_on_disk().get(key, {}) label = _escape_tags(str(profile.get("emulator") or key)) - if profile.get("type") in ("alias", "test"): + if profile.get("type") == "alias" or profile.get("role") == "test": return label return f"[{label}]({prefix}emulators/{key}.md)" @@ -4087,7 +4087,7 @@ def _profiles_by_core_name() -> dict[str, list[str]]: @functools.lru_cache(maxsize=None) def _core_names_in(emulators_dir: str) -> dict[str, list[str]]: profiles = _profiles_in(emulators_dir, skip_aliases=True) - with_page = {k for k, v in profiles.items() if v.get("type") not in ("alias", "test")} + with_page = {k for k, v in profiles.items() if v.get("type") != "alias" and v.get("role") != "test"} return { core: sorted(k for k in keys if k in with_page) for core, keys in upstream_profile_index(profiles).items() @@ -4920,7 +4920,7 @@ def _data_member_owners(profiles: dict) -> dict[str, str]: """ owners: dict[str, str] = {} for name in sorted(profiles): - if profiles[name].get("type") in ("alias", "test"): + if profiles[name].get("type") == "alias" or profiles[name].get("role") == "test": continue for tree in page_data_directories(name, profiles): owners.setdefault(str(tree.get("ref", "")), name) @@ -5145,7 +5145,7 @@ def file_lookup_records( **({"sha1": archive} if archive else {}), }) for key, profile in sorted(profiles.items()): - if profile.get("type") in ("alias", "test"): + if profile.get("type") == "alias" or profile.get("role") == "test": continue entries = profile.get("files") or [] own_systems = [str(s) for s in profile.get("systems") or []] @@ -5396,11 +5396,11 @@ def _folder_emulator(folder: str) -> tuple[str, str]: named after, by its profile key or its name, else the folder and "". """ profiles = _profiles_on_disk() - if folder in profiles and profiles[folder].get("type") not in ("alias", "test"): + if folder in profiles and profiles[folder].get("type") != "alias" and profiles[folder].get("role") != "test": return str(profiles[folder].get("emulator") or folder), folder for key in sorted(profiles): profile = profiles[key] - if profile.get("type") in ("alias", "test"): + if profile.get("type") == "alias" or profile.get("role") == "test": continue names = {str(profile.get(field) or "").casefold() for field in ("emulator", "display_name")} if folder.casefold() in names: @@ -7857,7 +7857,7 @@ def emulator_initial(display: str) -> str: def generate_emulators_index(profiles: dict) -> str: unique = { k: v for k, v in profiles.items() - if v.get("type") not in ("alias", "test") + if v.get("type") != "alias" and v.get("role") != "test" } aliases = {k: v for k, v in profiles.items() if v.get("type") == "alias"} @@ -8660,7 +8660,7 @@ def emulator_romset(profile: dict) -> str: 1.38.1 on MAME 0.289), else the version of a MAME build or of a FinalBurn core, whose sets follow its own release. """ - if profile.get("type") in ("alias", "launcher", "test"): + if profile.get("type") in ("alias", "launcher") or profile.get("role") == "test": return "" emulator = str(profile.get("emulator", "")) version = str(profile.get("core_version") or "") @@ -8939,7 +8939,7 @@ def emulator_install_route(name: str, profile: dict) -> tuple[str, list[str], tu the systems both run on: RetroArch's list names AetherSX2, and the command filled RetroArch's system folder, which the app never reads. """ - if profile.get("type") in ("alias", "launcher", "test"): + if profile.get("type") in ("alias", "launcher") or profile.get("role") == "test": return "", [], () if not [f for f in profile.get("files") or [] if isinstance(f, dict) and not comes_with_app(f)]: return "", [], () @@ -9873,7 +9873,7 @@ def plan_emulator_pages(profiles: dict, ``available`` is the test the pages use for a held file.""" stems = set() for name, profile in sorted(profiles.items()): - if profile.get("type") in ("alias", "test"): + if profile.get("type") == "alias" or profile.get("role") == "test": continue files = profile.get("files") or [] anchors = emulator_file_anchors(files) @@ -11046,7 +11046,7 @@ def generate_gap_analysis( unique_profiles = { k: v for k, v in profiles.items() - if v.get("type") not in ("alias", "test") + if v.get("type") != "alias" and v.get("role") != "test" } relevant_set: set[str] = set() for _name, cov in coverages.items(): @@ -11139,7 +11139,7 @@ def generate_gap_analysis( unique_profiles = { k: v for k, v in profiles.items() - if v.get("type") not in ("alias", "test") + if v.get("type") != "alias" and v.get("role") != "test" } for pname in coverages: config = coverages[pname]["config"] @@ -11547,7 +11547,7 @@ def generate_cross_reference( ``moved`` collects one page per platform with its core table. """ unique = { - k: v for k, v in profiles.items() if v.get("type") not in ("alias", "test") + k: v for k, v in profiles.items() if v.get("type") != "alias" and v.get("role") != "test" } total_cores = len(unique) @@ -12591,7 +12591,7 @@ def _generate(args: argparse.Namespace) -> None: _DATA_MEMBERS.update(data_directory_members()) _DATA_REVISIONS.update(data_directory_revisions()) for key in sorted(profiles): - if profiles[key].get("type") in ("alias", "test"): + if profiles[key].get("type") == "alias" or profiles[key].get("role") == "test": continue display = str(profiles[key].get("emulator", key)) for tree in page_data_directories(key, profiles): @@ -12607,7 +12607,7 @@ def _generate(args: argparse.Namespace) -> None: public_profiles = { name: profile for name, profile in profiles.items() - if profile.get("type") not in ("alias", "test") + if profile.get("type") != "alias" and profile.get("role") != "test" } (public / "api" / "v1" / "emulators").mkdir(parents=True, exist_ok=True) for name, profile in public_profiles.items(): diff --git a/scripts/pipeline.py b/scripts/pipeline.py index 94591e371..1cbf61378 100644 --- a/scripts/pipeline.py +++ b/scripts/pipeline.py @@ -627,7 +627,7 @@ def check_emulator_packs( systems = sorted({ _norm_system_id(str(sid)) for profile in profiles.values() - if profile.get("type") not in ("launcher", "alias", "test") + if profile.get("type") not in ("launcher", "alias") and profile.get("role") != "test" for sid in profile.get("systems") or [] }) systems_ok = True diff --git a/scripts/profile_sync.py b/scripts/profile_sync.py index abe7b5bf8..b10c2531c 100644 --- a/scripts/profile_sync.py +++ b/scripts/profile_sync.py @@ -4453,7 +4453,7 @@ def select_profiles(profiles: dict, args: argparse.Namespace) -> dict: # alias and test profiles too, "lists all 553" pointed at 530 lines. listed = sorted( name for name, profile in profiles.items() - if profile.get("type") not in ("alias", "test") + if profile.get("type") != "alias" and profile.get("role") != "test" ) raise SystemExit( f"ERROR: unknown profile {', '.join(unknown)}. " @@ -4466,7 +4466,7 @@ def select_profiles(profiles: dict, args: argparse.Namespace) -> dict: wanted_system = _norm_system_id(args.system) if args.system else None selected = {} for name, profile in sorted(profiles.items()): - if profile.get("type") in ("alias", "test"): + if profile.get("type") == "alias" or profile.get("role") == "test": continue if wanted_system and wanted_system not in { _norm_system_id(str(s)) for s in profile.get("systems") or [] diff --git a/scripts/sitecollection.py b/scripts/sitecollection.py index 08ad817ba..bdb154dec 100644 --- a/scripts/sitecollection.py +++ b/scripts/sitecollection.py @@ -97,7 +97,7 @@ def _engine_sections(manufacturers: dict[str, dict[str, list]]) -> dict[str, dic from siterender import _PROGRAM_TOPS, SECTION_LABELS, record_card_move # noqa: PLC0415 profiles = {key: profile for key, profile in _profiles_on_disk().items() - if profile.get("type") not in ("alias", "test")} + if profile.get("type") != "alias" and profile.get("role") != "test"} readers: dict[str, set[str]] = {} for key, profile in profiles.items(): for entry in profile.get("files") or []: diff --git a/scripts/siterender.py b/scripts/siterender.py index fb84818db..1bd66393d 100644 --- a/scripts/siterender.py +++ b/scripts/siterender.py @@ -423,7 +423,7 @@ def _build_boards() -> None: ids_by_name: dict[str, set[str]] = {} for key in sorted(profiles): profile = profiles[key] - if profile.get("type") == "test": + if profile.get("role") == "test": continue systems = [str(sys_id) for sys_id in profile.get("systems") or []] for entry in profile.get("files") or []: diff --git a/tests/test_profile_names.py b/tests/test_profile_names.py index 5a9e011e2..0883422a1 100644 --- a/tests/test_profile_names.py +++ b/tests/test_profile_names.py @@ -21,10 +21,10 @@ from common import load_emulator_profiles # noqa: E402 STEM_NAMED = { # Template and test cores, no emulator page. "00_example": "template", - "test": "type test", - "testgl": "type test", - "testvulkan": "type test", - "vidtest": "type test", + "test": "role test", + "testgl": "role test", + "testvulkan": "role test", + "vidtest": "role test", # Aliases, no emulator page. "ep128emu": "alias", "mednafen_gba": "alias", diff --git a/tests/test_site_cores.py b/tests/test_site_cores.py index 9bc8c3d70..d6891bcfe 100644 --- a/tests/test_site_cores.py +++ b/tests/test_site_cores.py @@ -30,7 +30,7 @@ class SiteCoreCounts(unittest.TestCase): def test_each_platform_count_is_the_resolved_set(self): platforms = str(REPO_ROOT / "platforms") profiles = load_emulator_profiles(str(REPO_ROOT / "emulators")) - unique = {k: v for k, v in profiles.items() if v.get("type") not in ("alias", "test")} + unique = {k: v for k, v in profiles.items() if v.get("type") != "alias" and v.get("role") != "test"} coverages = {} for name in list_registered_platforms(platforms): config = load_platform_config(name, platforms) diff --git a/tests/test_site_emulators.py b/tests/test_site_emulators.py index e5a49b586..0683d7aee 100644 --- a/tests/test_site_emulators.py +++ b/tests/test_site_emulators.py @@ -499,7 +499,7 @@ class TheTemplateIsNoEmulator(unittest.TestCase): self.assertNotIn("00_example", index) self.assertIn("melonds.md", index) # A test profile gets no page at all (generate_site's public_profiles). - self.assertEqual(_profile("00_example")["type"], "test") + self.assertEqual(_profile("00_example")["role"], "test") @unittest.skipUnless((DIST / "emulators" / "aethersx2" / "index.html").is_file(), "site not built") @@ -517,7 +517,7 @@ class TheBuiltEmulatorPages(unittest.TestCase): self.assertNotIn("RetroArch", page.split("Install this emulator", 1)[1].split("", 1)[0]) def test_the_index_leaves_the_template_out(self): - # 00_example is `type: test`: no page, no index row. + # 00_example has `role: test`: no page, no index row. self.assertNotIn("00_example", self._html("emulators")) self.assertIn('data-pagefind-body', self._html("emulators/melonds")) self.assertFalse((DIST / "emulators" / "00_example").exists()) @@ -630,7 +630,7 @@ class DataFolderPrinted(unittest.TestCase): if not _generated(): self.skipTest("site pages not generated") for name, profile in _profiles_with_data_root().items(): - if profile.get("type") in ("alias", "test"): + if profile.get("type") == "alias" or profile.get("role") == "test": continue page = _built_page(name) with self.subTest(profile=name): diff --git a/tests/test_site_integration.py b/tests/test_site_integration.py index b0904d34c..589ca9fc7 100644 --- a/tests/test_site_integration.py +++ b/tests/test_site_integration.py @@ -55,7 +55,7 @@ class EngineIntros(unittest.TestCase): def test_every_intro_names_what_it_emulates(self): for path in sorted(glob.glob(str(REPO_ROOT / "emulators" / "*.yml"))): profile = yaml.safe_load(Path(path).read_text(encoding="utf-8")) - if not isinstance(profile, dict) or profile.get("type") in ("alias", "test"): + if not isinstance(profile, dict) or profile.get("type") == "alias" or profile.get("role") == "test": continue name = Path(path).stem with self.subTest(profile=name): diff --git a/tests/test_site_reference.py b/tests/test_site_reference.py index e3431cfc5..ee73024c0 100644 --- a/tests/test_site_reference.py +++ b/tests/test_site_reference.py @@ -747,7 +747,7 @@ class AnIdLeadsToItsData(unittest.TestCase): "pcsx2": {"type": "standalone"}, "lrps2": {"type": "libretro", "cores": ["pcsx2"]}, "mednafen_psx_alias": {"type": "alias", "alias_of": "beetle_psx"}, - "probe": {"type": "test", "cores": ["probe_core"]}, + "probe": {"type": "libretro", "role": "test", "cores": ["probe_core"]}, } items = generate_site.core_index_items(profiles) by_name: dict[str, list[tuple[str, bool]]] = {} diff --git a/tests/test_target_names.py b/tests/test_target_names.py index cb08e96b2..c3939fdde 100644 --- a/tests/test_target_names.py +++ b/tests/test_target_names.py @@ -21,6 +21,36 @@ sys.path.insert(0, str(REPO_ROOT / "scripts")) from common import canonical_target_name # noqa: E402 +class EveryBuildbotCoreIsNamed(unittest.TestCase): + """A core RetroArch installs that its manifest does not name is "a core + this list does not know" to --installed-cores (issue 87). 2048, gong, + radio, video_processor and test were typed by their role, which hid + their libretro build from all_libretro.""" + + def test_retroarch_names_every_buildbot_core(self): + import yaml # noqa: PLC0415 + + from common import ( # noqa: PLC0415 + load_emulator_profiles, + load_platform_config, + load_target_config, + resolve_platform_cores, + upstream_profile_index, + ) + + platforms = str(REPO_ROOT / "platforms") + profiles = load_emulator_profiles(str(REPO_ROOT / "emulators")) + runs = resolve_platform_cores(load_platform_config("retroarch", platforms), profiles) + index = upstream_profile_index(profiles, include_aliases=True) + targets = yaml.safe_load((REPO_ROOT / "platforms" / "targets" / "retroarch.yml").read_text()) + names = { + name for target in targets["targets"] + for name in load_target_config("retroarch", target, platforms) or () + } + unnamed = sorted(name for name in names if not (index.get(name, set()) & runs)) + self.assertEqual(unnamed, []) + + class AliasesResolveToOneName(unittest.TestCase): def test_retroarch_switch_aliases(self): platforms = str(REPO_ROOT / "platforms") diff --git a/wiki/profiling.md b/wiki/profiling.md index 69e6ea50e..d70046c21 100644 --- a/wiki/profiling.md +++ b/wiki/profiling.md @@ -365,9 +365,12 @@ A few field conventions that protect the toolchain: - `type:` is operational. `resolve_platform_cores()` uses it to filter which profiles apply to a platform. Valid values are `libretro`, `standalone + libretro`, `standalone`, `alias`, `launcher`, `game`, - `utility`, `test`. Putting a classification concept here (like + `utility`. Putting a classification concept here (like "bizhawk-native") breaks the filtering. A BizHawk core is - `type: standalone`. + `type: standalone`. A libretro core that emulates no machine stays + `type: libretro` and says what it is for in `role:` (`game`, `utility` + or `test`): typed by its role, 2048 fell out of every all_libretro + platform and RetroArch's manifest no longer named it. - `core_classification:` is descriptive. It documents the relationship between the core and the original emulator (pure_libretro, @@ -513,7 +516,8 @@ reports the profile as uncheckable, and the CI job that runs | Field | Required | Description | |-------|----------|-------------| | `emulator` | yes | display name | -| `type` | yes | `libretro`, `standalone`, `standalone + libretro`, `alias`, `launcher`, `game`, `utility`, `test` | +| `type` | yes | `libretro`, `standalone`, `standalone + libretro`, `alias`, `launcher`, `game`, `utility` | +| `role` | no | `game`, `utility`, `test`: what a libretro core that emulates no machine is for. The site and coverage leave `test` cores out | | `core_classification` | no | `pure_libretro`, `official_port`, `community_fork`, `frozen_snapshot`, `enhanced_fork`, `game_engine`, `embedded_hle`, `launcher`, `alias`, `other` | | `source` | no | libretro core repository URL. A dict when the core ships under several repos | | `upstream` | no | original emulator repository URL |