diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 9ab1508c..02b1ab36 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -5,6 +5,7 @@ on: paths: - "bios/**" - "platforms/**" + - "emulators/**" permissions: contents: read @@ -94,6 +95,34 @@ jobs: sys.exit(1) " + - name: Validate emulator profiles + run: | + python -c " + import json, yaml, sys + from jsonschema import validate, ValidationError + from pathlib import Path + + with open('schemas/emulator.schema.json') as f: + schema = json.load(f) + + errors = [] + for yml_file in sorted(Path('emulators').glob('*.yml')): + if yml_file.name.endswith('.old.yml'): + continue + with open(yml_file) as f: + profile = yaml.safe_load(f) + try: + validate(profile, schema) + except ValidationError as e: + path = '/'.join(str(p) for p in e.absolute_path) + errors.append(f'{yml_file.name}: {path}: {e.message}') + print(f'FAIL {yml_file.name}: {path}: {e.message}') + + print(f'{len(errors)} invalid profiles') + if errors: + sys.exit(1) + " + run-tests: runs-on: ubuntu-latest steps: diff --git a/emulators/freeintv_ts_overlay.yml b/emulators/freeintv_ts_overlay.yml index 2fb47a75..ed7ba784 100644 --- a/emulators/freeintv_ts_overlay.yml +++ b/emulators/freeintv_ts_overlay.yml @@ -1,5 +1,6 @@ emulator: "FreeIntv (Touchscreen Overlay)" type: alias +alias_of: freeintv core_classification: alias source: "https://github.com/jcarr71/FreeIntvTSOverlay" upstream: "https://github.com/libretro/FreeIntv" diff --git a/emulators/trident.yml b/emulators/trident.yml index af432eb0..34288166 100644 --- a/emulators/trident.yml +++ b/emulators/trident.yml @@ -4,7 +4,7 @@ type: libretro core_classification: community_fork source: https://github.com/DanAlexMorton/3dsTrident upstream: https://github.com/wheremyfoodat/Panda3DS -profiled_date: 2026-03-25 +profiled_date: "2026-03-25" core_version: git cores: diff --git a/emulators/zc210.yml b/emulators/zc210.yml index cb988734..e785c229 100644 --- a/emulators/zc210.yml +++ b/emulators/zc210.yml @@ -5,7 +5,7 @@ source: https://github.com/netux79/zc210-libretro upstream: https://www.zeldaclassic.com/ core_version: "2.10 Beta 5" display_name: Zelda Classic v2.10 (ZC210) -profiled_date: 2026-03-26 +profiled_date: "2026-03-26" cores: - zc210 systems: diff --git a/schemas/emulator.schema.json b/schemas/emulator.schema.json index bdcc2032..2072b9dd 100644 --- a/schemas/emulator.schema.json +++ b/schemas/emulator.schema.json @@ -1,19 +1,72 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Emulator BIOS Profile", + "description": "One YAML per emulator core or engine, documenting every external file the code loads. Field reference: https://abdess.github.io/retrobios/wiki/profiling/", "type": "object", - "required": ["emulator", "source", "systems", "files"], + "required": ["emulator", "type"], + "allOf": [ + { + "comment": "An alias profile points at another profile and carries no files of its own.", + "if": { + "properties": { "type": { "const": "alias" } }, + "required": ["type"] + }, + "then": { "required": ["alias_of"] }, + "else": { "required": ["files"] } + } + ], "properties": { "emulator": { "type": "string" }, + "display_name": { "type": "string" }, "type": { + "description": "Operational: resolve_platform_cores() filters on it. Not a classification.", "type": "string", - "enum": ["standalone", "libretro", "standalone + libretro"] + "enum": [ + "libretro", + "standalone", + "standalone + libretro", + "alias", + "launcher", + "game", + "utility", + "test" + ] }, - "source": { "type": "string", "format": "uri" }, - "systems": { - "type": "array", - "items": { "type": "string" } + "core_classification": { + "description": "Descriptive only, no effect on tooling.", + "type": "string", + "enum": [ + "pure_libretro", + "official_port", + "community_fork", + "frozen_snapshot", + "enhanced_fork", + "game_engine", + "embedded_hle", + "launcher", + "alias", + "other" + ] }, + "source": { "type": ["string", "object"] }, + "upstream": { "type": ["string", "object", "null"] }, + "source_commit": { "type": "string" }, + "upstream_commit": { "type": "string" }, + "profiled_date": { + "description": "Quote it: an unquoted YAML date parses to a date object.", + "type": ["string", "object"] + }, + "core_version": { "type": ["string", "number", "object", "null"] }, + "alias_of": { "type": "string" }, + "cores": { + "description": "Upstream names the core is known by. A dict maps each name to per-variant metadata.", + "type": ["array", "object"] + }, + "systems": { "type": "array", "items": { "type": "string" } }, + "mode": { "type": "string", "enum": ["standalone", "libretro", "both"] }, + "verification": { "type": "string", "enum": ["existence", "md5", "sha1", "crc32"] }, + "exclusion_note": { "type": "string" }, + "notes": { "type": ["string", "array", "object"] }, "files": { "type": "array", "items": { @@ -21,13 +74,70 @@ "required": ["name"], "properties": { "name": { "type": "string" }, - "region": { "type": "string" }, - "required": { "type": "boolean", "default": false }, - "md5": { "type": "string", "pattern": "^[a-fA-F0-9]{32}$" }, - "sha1": { "type": "string", "pattern": "^[a-f0-9]{40}$" }, - "size": { "type": "integer" }, - "source_ref": { "type": "string" }, - "note": { "type": "string" } + "required": { "type": "boolean" }, + "system": { "type": "string" }, + "path": { "type": ["string", "null"] }, + "destination": { "type": "string" }, + "aliases": { "type": "array", "items": { "type": "string" } }, + "archive": { + "description": "ROM set the file lives in, for arcade entries.", + "type": "string" + }, + "category": { + "type": "string", + "enum": ["bios", "game_data", "bios_zip"] + }, + "mode": { "type": "string", "enum": ["standalone", "libretro", "both"] }, + "storage": { + "type": "string", + "enum": ["embedded", "external", "user_provided", "large_file", "release"] + }, + "hle_fallback": { "type": "boolean" }, + "agnostic": { "type": "boolean" }, + "size": { "type": ["integer", "array", "null"] }, + "min_size": { "type": "integer" }, + "max_size": { "type": "integer" }, + "md5": { + "oneOf": [ + { "type": "null" }, + { "type": "string", "pattern": "^[a-fA-F0-9]{32}$" }, + { + "type": "array", + "items": { "type": "string", "pattern": "^[a-fA-F0-9]{32}$" } + } + ] + }, + "sha1": { + "oneOf": [ + { "type": "string", "pattern": "^[a-fA-F0-9]{40}$" }, + { + "type": "array", + "items": { "type": "string", "pattern": "^[a-fA-F0-9]{40}$" } + } + ] + }, + "sha256": { + "type": "string", + "pattern": "^[a-fA-F0-9]{64}(,[a-fA-F0-9]{64})*$" + }, + "crc32": { "type": "string" }, + "known_hash_adler32": { "type": ["string", "integer"] }, + "validation": { + "description": "Checks the code performs. A dict splits core from upstream when they diverge.", + "oneOf": [ + { "type": "array", "items": { "type": "string" } }, + { "type": "object" } + ] + }, + "region": { "type": ["string", "array"] }, + "contents": { "type": "array", "items": { "type": "object" } }, + "source_ref": { + "description": "A dict splits core from upstream when the two diverge.", + "type": ["string", "array", "object"] + }, + "description": { "type": "string" }, + "note": { "type": "string" }, + "unsourceable": { "type": "string" } } } }