mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-06 01:53:56 -06:00
feat: add region and per-game file rules
This commit is contained in:
1 parent
8d8689c2e4
commit
dc337c893c
11 files changed
+1293
No files matched your search
@@ -0,0 +1,278 @@
|
||||
# Files a game needs beyond its system's region BIOS, decided by the game.
|
||||
#
|
||||
# Each entry names the core (profile), the system, what in the game
|
||||
# decides it (`trigger`), and the files by the names the profile declares.
|
||||
# `required: true` means the code refuses or fails the game without the
|
||||
# file. A trigger is one of:
|
||||
# extensions the file's lowercased extension
|
||||
# header bytes of the game: `reader` names a reader of games/headers.yml and
|
||||
# `values` the labels or values it must return (`field` for one of its fields)
|
||||
# every_game every game of the system, with `one_of` when any single file serves
|
||||
# `filename_override: true` marks a choice the core also takes from tags in
|
||||
# the file name: a tag is a guess, so the installer keeps every file listed
|
||||
# for that system and core.
|
||||
#
|
||||
# Every source_ref names the profile's own pin.
|
||||
|
||||
extras:
|
||||
- system: nintendo-fds
|
||||
profile: fceumm
|
||||
trigger:
|
||||
header:
|
||||
reader: fds-image
|
||||
values: [fds]
|
||||
files: [disksys.rom]
|
||||
required: true
|
||||
source_ref: "libretro/libretro-fceumm@ac0d00a7 src/fds.c:624-625, src/fds.c:680-690, src/fceu.c:275-281"
|
||||
- system: nintendo-fds
|
||||
profile: mesen
|
||||
trigger:
|
||||
header:
|
||||
reader: fds-image
|
||||
values: [fds]
|
||||
files: [disksys.rom]
|
||||
required: true
|
||||
source_ref: "libretro/Mesen@0102910c Core/RomLoader.cpp:40, Core/FdsLoader.cpp:44-58"
|
||||
- system: nintendo-fds
|
||||
profile: nestopia
|
||||
trigger:
|
||||
extensions: [.fds]
|
||||
files: [disksys.rom]
|
||||
required: true
|
||||
source_ref: "libretro/nestopia@2110d21b libretro/libretro.cpp:2171, libretro/libretro.cpp:2182"
|
||||
|
||||
- system: nintendo-satellaview
|
||||
profile: snes9x
|
||||
trigger:
|
||||
header:
|
||||
reader: snes-bsx-header
|
||||
values: [bsx]
|
||||
files: [BS-X.bin]
|
||||
required: true
|
||||
source_ref: "libretro/snes9x@fae2fea0 libretro/libretro.cpp:1417, libretro/libretro.cpp:1511-1514"
|
||||
- system: nintendo-satellaview
|
||||
profile: bsnes
|
||||
trigger:
|
||||
extensions: [.bs]
|
||||
files: [BS-X.bin]
|
||||
required: true
|
||||
source_ref: "libretro/bsnes-libretro@05439f96 bsnes/target-libretro/libretro.cpp:955-966"
|
||||
|
||||
- system: nintendo-sufami-turbo
|
||||
profile: snes9x
|
||||
trigger:
|
||||
header:
|
||||
reader: sufami-magic
|
||||
values: [sufami-turbo]
|
||||
files: [STBIOS.bin]
|
||||
required: true
|
||||
source_ref: "libretro/snes9x@fae2fea0 libretro/libretro.cpp:1458-1462, libretro/libretro.cpp:1505-1507"
|
||||
|
||||
- system: nintendo-sgb
|
||||
profile: bsnes
|
||||
trigger:
|
||||
extensions: [.gb, .gbc]
|
||||
files: [SGB1.sfc]
|
||||
required: true
|
||||
note: SGB1.sfc is the default of the bsnes_sgb_bios option, SGB2.sfc when the user picks it.
|
||||
source_ref: "libretro/bsnes-libretro@05439f96 bsnes/target-libretro/libretro.cpp:937-952, bsnes/target-libretro/libretro.cpp:395-400, bsnes/target-libretro/libretro_core_options.h:689-693"
|
||||
- system: nintendo-sgb
|
||||
profile: mgba
|
||||
trigger:
|
||||
header:
|
||||
reader: gb-sgb-flag
|
||||
values: [sgb]
|
||||
files: [sgb_bios.bin]
|
||||
required: false
|
||||
source_ref: "libretro/mgba@7a12d6d4 src/gb/gb.c:938-946, src/platform/libretro/libretro.c:2066-2116"
|
||||
|
||||
- system: nintendo-64dd
|
||||
profile: mupen64plus_next
|
||||
trigger:
|
||||
extensions: [.ndd]
|
||||
files: [IPL.n64]
|
||||
required: true
|
||||
note: The disk reaches the core through the N64 Disk Drive subsystem or a <game>.ndd beside the cartridge, never as content of its own.
|
||||
source_ref: "libretro/mupen64plus-libretro-nx@15bada85 mupen64plus-core/src/main/main.c:953-985, libretro/libretro.c:643, libretro/libretro.c:1890-1904, libretro/libretro.c:658"
|
||||
- system: nintendo-64dd
|
||||
profile: parallel_n64
|
||||
trigger:
|
||||
extensions: [.ndd]
|
||||
files: [64DD_IPL.bin]
|
||||
required: false
|
||||
source_ref: "libretro/parallel-n64@f87550d9 libretro/libretro.c:1370, libretro/libretro.c:951"
|
||||
- system: nintendo-64dd
|
||||
profile: ares
|
||||
trigger:
|
||||
extensions: [.ndd]
|
||||
region_files: true
|
||||
required: true
|
||||
note: The IPL follows the disk's region word, see games/regions.yml.
|
||||
source_ref: "ares-emulator/ares@d400eee9 mia/medium/nintendo-64dd.cpp:57-77, desktop-ui/emulator/nintendo-64dd.cpp:64-80"
|
||||
- system: nintendo-64dd
|
||||
profile: project64
|
||||
trigger:
|
||||
extensions: [.ndd]
|
||||
region_files: true
|
||||
required: true
|
||||
source_ref: "project64/project64@24376e86 Source/Project64-core/N64System/N64Disk.cpp:828-840, Source/Project64-core/N64System/N64System.cpp:641-662"
|
||||
- system: nintendo-64dd
|
||||
profile: rosalies-mupen-gui
|
||||
trigger:
|
||||
extensions: [.ndd]
|
||||
region_files: true
|
||||
required: true
|
||||
source_ref: "Rosalie241/RMG@4777ed3f Source/3rdParty/mupen64plus-core/src/device/dd/disk.c:405-480, Source/RMG-Core/MediaLoader.cpp:47-63"
|
||||
|
||||
- system: msx
|
||||
profile: fmsx
|
||||
trigger:
|
||||
extensions: [.dsk, .fdi]
|
||||
files: [DISK.ROM]
|
||||
required: true
|
||||
source_ref: "libretro/fmsx-libretro@4de11755 libretro.c:1220-1225, libretro.c:1303-1304, fMSX/MSX.c:514, fMSX/MSX.c:778-791"
|
||||
- system: msx2plus
|
||||
profile: bluemsx
|
||||
trigger:
|
||||
extensions: [.dsk]
|
||||
files: [MSX2P.rom, MSX2PEXT.rom, MSX2PMUS.rom, PANASONICDISK.rom]
|
||||
required: true
|
||||
note: With the Auto machine a disk picks MSX2+ and never falls back to C-BIOS. The machine's ROM list is the profile's, from Machines/MSX2+/config.ini.
|
||||
source_ref: "libretro/blueMSX-libretro@e3086eb5 libretro.c:156-235, libretro.c:1410-1446"
|
||||
|
||||
- system: snk-neogeo-cd
|
||||
profile: neocd
|
||||
trigger:
|
||||
every_game: true
|
||||
one_of: true
|
||||
files: [neocd_f.rom, front-sp1.bin, neocd_sf.rom, neocd_t.rom, top-sp1.bin, neocd_st.rom, neocd_z.rom, neocd.bin, neocd_sz.rom, uni-bioscd.rom]
|
||||
required: true
|
||||
note: Drive models, not regions. The core option picks among the files found, the first listed by default.
|
||||
source_ref: "libretro/neocd_libretro@b1e04c73 src/libretro_variables.cpp:143-176, src/libretro_variables.cpp:244-273, src/memory_cdintf.cpp:111-116"
|
||||
|
||||
- system: nintendo-ds
|
||||
profile: melonds_ds
|
||||
trigger:
|
||||
header:
|
||||
reader: nds-secure-area
|
||||
values: [encrypted]
|
||||
files: [bios7.bin, bios9.bin]
|
||||
required: true
|
||||
source_ref: "JesseTG/melonds-ds@f394adba src/libretro/config/console.cpp:379-392, src/libretro/config/console.cpp:419-424"
|
||||
- system: nintendo-dsi
|
||||
profile: melonds_ds
|
||||
trigger:
|
||||
header:
|
||||
reader: nds-unit-code
|
||||
values: [dsi]
|
||||
files: [dsi_bios7.bin, dsi_bios9.bin, dsi_firmware.bin, dsi_nand.bin]
|
||||
required: true
|
||||
source_ref: "JesseTG/melonds-ds@f394adba src/libretro/config/console.cpp:238-256"
|
||||
|
||||
- system: nec-pc-engine
|
||||
profile: mednafen_pce_fast
|
||||
trigger:
|
||||
extensions: [.cue, .ccd, .chd, .toc, .m3u]
|
||||
files: [syscard3.pce]
|
||||
required: true
|
||||
note: syscard3.pce is the default of the pce_fast_cdbios option. HuCards need no file.
|
||||
source_ref: "libretro/beetle-pce-fast-libretro@4eee99c4 libretro.c:1620-1628, libretro.c:65, libretro.c:1283-1290, libretro.c:1172-1180"
|
||||
- system: nec-pc-engine
|
||||
profile: beetle_pce
|
||||
trigger:
|
||||
extensions: [.cue, .ccd, .chd, .toc, .m3u]
|
||||
files: [syscard3.pce]
|
||||
required: true
|
||||
note: syscard3.pce is the default of the pce_cdbios option. HuCards need no file.
|
||||
source_ref: "libretro/beetle-pce-libretro@b96c11e0 libretro.cpp:1047-1063, libretro_core_options.h:659-674"
|
||||
- system: nec-pc-engine
|
||||
profile: beetle_pce
|
||||
trigger:
|
||||
header:
|
||||
reader: pce-games-express
|
||||
values: [games-express]
|
||||
files: [gexpress.pce]
|
||||
required: true
|
||||
source_ref: "libretro/beetle-pce-libretro@b96c11e0 mednafen/pce/pce.cpp:374-410, mednafen/pce/pce.cpp:447-455"
|
||||
- system: nec-pc-engine-cd
|
||||
profile: geargrafx
|
||||
trigger:
|
||||
every_game: true
|
||||
files: [syscard3.pce]
|
||||
required: true
|
||||
note: syscard3.pce is the default of the geargrafx_cdrom_bios option.
|
||||
source_ref: "drhelius/Geargrafx@7c447b0b platforms/libretro/libretro.cpp:761-802, platforms/libretro/libretro.cpp:1645-1668"
|
||||
- system: nec-pc-engine-cd
|
||||
profile: geargrafx
|
||||
trigger:
|
||||
every_game: true
|
||||
files: [gexpress.pce]
|
||||
required: false
|
||||
note: Loaded beside the System Card whenever present.
|
||||
source_ref: "drhelius/Geargrafx@7c447b0b platforms/libretro/libretro.cpp:761-802, platforms/libretro/libretro.cpp:1645-1668"
|
||||
|
||||
- system: sega-dreamcast
|
||||
profile: flycast
|
||||
trigger:
|
||||
header:
|
||||
reader: dc-product-id
|
||||
values: ["T7001D 50", "T40505D 50", T18702M, T0019M, T0020M, HDR-0187, "T15104D 50", MK-51152]
|
||||
files: [dc_boot.bin]
|
||||
required: true
|
||||
note: For these product ids the core asks for the real BIOS and warns "This game requires a real BIOS" when it boots HLE.
|
||||
source_ref: "flyinghead/flycast@e88b0ced core/emulator.cpp:267-279, core/emulator.cpp:613-622"
|
||||
- system: sega-dreamcast
|
||||
profile: flycast
|
||||
trigger:
|
||||
header:
|
||||
reader: dc-product-id
|
||||
field: disk_type
|
||||
values: [MIL CD]
|
||||
files: [dc_boot.bin]
|
||||
required: true
|
||||
note: Every MIL-CD disc asks for the real BIOS, as the product ids above do.
|
||||
source_ref: "flyinghead/flycast@e88b0ced core/emulator.cpp:275-279, core/emulator.cpp:613-622"
|
||||
|
||||
- system: commodore-amiga
|
||||
profile: puae
|
||||
trigger:
|
||||
extensions: [.adf, .adz, .fdi, .dms, .raw, .ipf]
|
||||
files: [kick34005.A500]
|
||||
required: true
|
||||
filename_override: true
|
||||
source_ref: "libretro/libretro-uae@73879370 libretro/libretro-dc.c:847-882, libretro/libretro-core.c:865-917, libretro/libretro-core.c:6526-6601, libretro/libretro-core.c:5609-5679"
|
||||
- system: commodore-amiga
|
||||
profile: puae
|
||||
trigger:
|
||||
extensions: [.hdf, .hdz, .lha, .slave, .info]
|
||||
files: [kick40068.A1200]
|
||||
required: true
|
||||
filename_override: true
|
||||
source_ref: "libretro/libretro-uae@73879370 libretro/libretro-dc.c:847-882, libretro/libretro-core.c:865-917, libretro/libretro-core.c:6526-6601, libretro/libretro-core.c:5609-5679"
|
||||
- system: commodore-amiga-cd32
|
||||
profile: puae
|
||||
trigger:
|
||||
extensions: [.cue, .ccd, .nrg, .mds, .iso, .chd]
|
||||
files: [kick40060.CD32, kick40060.CD32.ext]
|
||||
required: true
|
||||
filename_override: true
|
||||
source_ref: "libretro/libretro-uae@73879370 libretro/libretro-dc.c:847-882, libretro/libretro-core.c:865-917, libretro/libretro-core.c:6526-6601, libretro/libretro-core.c:5609-5679"
|
||||
|
||||
- system: atari-jaguar
|
||||
profile: virtualjaguar
|
||||
trigger:
|
||||
extensions: [.cue, .cdi, .chd]
|
||||
files: ["[BIOS] Atari Jaguar CD (World).j64"]
|
||||
required: false
|
||||
note: HLE boot by default, and an embedded CD BIOS when the real one is absent.
|
||||
source_ref: "libretro/virtualjaguar-libretro@e7db5367 libretro.c:113, libretro.c:7081, libretro.c:6587-6604"
|
||||
|
||||
- system: atari-lynx
|
||||
profile: handy
|
||||
trigger:
|
||||
header:
|
||||
reader: lynx-header
|
||||
values: [headerless]
|
||||
files: [howard.o]
|
||||
required: false
|
||||
source_ref: "libretro/libretro-handy@bc55d462 lynx/cart.cpp:283-296, lynx/system.cpp:197-238"
|
||||
@@ -0,0 +1,327 @@
|
||||
# Which BIOS region a core opens for a game, beyond what the profiles say.
|
||||
#
|
||||
# The files of each region come from the profiles' `region:` fields
|
||||
# (scripts/region.py build_region_index), never from this file. What is
|
||||
# recorded here is what the code does around them: the game regions it
|
||||
# sends to another region's file (`routes`), the region it falls back to
|
||||
# when the game names none it knows (`default_region`), the order it picks
|
||||
# from a multi-region disc (`preference`), the files it opens whatever the
|
||||
# region (`region_free`), whether a missing region file is replaced by
|
||||
# another region's (`fallback_across_regions`), and the cases where even a
|
||||
# proven game region does not settle the file (`keep_all_regions`).
|
||||
#
|
||||
# Only cores whose region code was read at the profile's pin are listed.
|
||||
# Every source_ref names the profile's own pin.
|
||||
|
||||
cases:
|
||||
user-region-option:
|
||||
title: A core option forces the console region
|
||||
reason: The forced region decides the file, and the option is device state the installer does not read.
|
||||
psx-override-bios:
|
||||
title: Region-free override BIOS
|
||||
reason: An option swaps in a region-free image for every region.
|
||||
duckstation-custom-region:
|
||||
title: Per-game custom region
|
||||
reason: A region the user set for one game in the game list replaces the region read from the disc.
|
||||
cross-region-fallback:
|
||||
title: Missing region file replaced by another region's image
|
||||
reason: The game still starts without the precise file, on another region's BIOS with a warning.
|
||||
first-disc-region:
|
||||
title: Only the first disc sets the region
|
||||
reason: For a multi-disc game the region is read from disc 1 alone.
|
||||
multi-area-disc:
|
||||
title: Disc listing several areas
|
||||
reason: The core picks among the disc's areas by its own preference order.
|
||||
saturn-kr-ipl:
|
||||
title: Asia NTSC and Korea discs ask for an undeclared IPL
|
||||
reason: The KR IPL is not declared by the profile, so the core falls back to a region-free image, then a mismatched one.
|
||||
saturn-disc-database:
|
||||
title: A disc or cartridge database overrides the header
|
||||
reason: The core's own database decides the area, so a header read is not enough without that table.
|
||||
sms-crc-list:
|
||||
title: CRC32 list overrides the cartridge header
|
||||
reason: A list of whole-ROM CRC32s forces Europe or Japan before the header is read.
|
||||
megacd-region-list:
|
||||
title: Mega CD region from the disc's region list
|
||||
reason: The file follows the disc's list of regions and the preferred-region option, not one header byte.
|
||||
gamecube-unknown-region:
|
||||
title: GameCube disc with an unknown region field
|
||||
reason: An unknown value at 0x458 falls back to the host locale, which differs per device.
|
||||
naomi-one-archive:
|
||||
title: One BIOS archive serves every region
|
||||
reason: The region filters blobs inside one archive, so the archive is the unit.
|
||||
nds-header-decides:
|
||||
title: NDS BIOS decided by header bytes, not region
|
||||
reason: An encrypted secure area or a DSi unit code decides the files, not the game region.
|
||||
optional-pif:
|
||||
title: Optional boot image
|
||||
reason: The core runs HLE without the file, and dropping it saves under 4 KB.
|
||||
|
||||
systems:
|
||||
sony-playstation:
|
||||
beetle_psx:
|
||||
routes:
|
||||
south-korea: japan
|
||||
default_region: north-america
|
||||
region_free: [psxonpsp660.bin, ps1_rom.bin, openbios.bin]
|
||||
fallback_across_regions: false
|
||||
source_ref: "libretro/beetle-psx-libretro@7fea57a5 libretro.c:2426-2446, libretro.c:2497-2540, libretro.c:768-821, libretro.c:3425-3458, mednafen/settings.c:42"
|
||||
keep_all_regions:
|
||||
- case: psx-override-bios
|
||||
condition: option
|
||||
option: beetle_psx_override_bios
|
||||
default: disabled
|
||||
keeps: region-free-files
|
||||
files: [psxonpsp660.bin, ps1_rom.bin, openbios.bin]
|
||||
source_ref: "libretro/beetle-psx-libretro@7fea57a5 libretro.c:694-765, libretro_core_options.h:259-274"
|
||||
- case: first-disc-region
|
||||
condition: disc
|
||||
when: multi-disc game (.m3u)
|
||||
keeps: first-disc
|
||||
source_ref: "libretro/beetle-psx-libretro@7fea57a5 libretro.c:2426-2446"
|
||||
swanstation:
|
||||
default_region: north-america
|
||||
region_free: [psxonpsp660.bin, ps1_rom.bin]
|
||||
fallback_across_regions: true
|
||||
source_ref: "libretro/swanstation@631f0fb3 src/core/system.cpp:400-456, src/core/system.cpp:621-640, src/core/host_interface.cpp:100-196, src/core/bios.cpp:28-71"
|
||||
keep_all_regions:
|
||||
- case: user-region-option
|
||||
condition: option
|
||||
option: swanstation_Console_Region
|
||||
default: Auto
|
||||
keeps: all-regions
|
||||
source_ref: "libretro/swanstation@631f0fb3 src/libretro/libretro_core_options.h:82-136"
|
||||
- case: cross-region-fallback
|
||||
condition: always
|
||||
keeps: precise
|
||||
source_ref: "libretro/swanstation@631f0fb3 src/core/host_interface.cpp:100-196"
|
||||
duckstation:
|
||||
default_region: north-america
|
||||
region_free: [openbios.bin]
|
||||
fallback_across_regions: true
|
||||
source_ref: "stenzek/duckstation@526b4a12 src/core/system.cpp:939-1009, src/core/system.cpp:664-680, src/core/bios.cpp:34-139, src/core/bios.cpp:397-460"
|
||||
keep_all_regions:
|
||||
- case: user-region-option
|
||||
condition: option
|
||||
option: Console/Region
|
||||
default: Auto
|
||||
keeps: all-regions
|
||||
source_ref: "stenzek/duckstation@526b4a12 src/core/system.cpp:1739-1771, src/core/settings.h:608"
|
||||
- case: duckstation-custom-region
|
||||
condition: user-state
|
||||
when: the user set a custom region for the game in the game list
|
||||
keeps: all-regions
|
||||
source_ref: "stenzek/duckstation@526b4a12 src/core/bios.cpp:397-460"
|
||||
pcsx_rearmed:
|
||||
default_region: north-america
|
||||
fallback_across_regions: true
|
||||
source_ref: "libretro/pcsx_rearmed@c8816799 libpcsxcore/misc.c:415-455, frontend/libretro.c:3604-3680, frontend/libretro.c:3864-3885, libpcsxcore/psxmem.c:291-327"
|
||||
keep_all_regions:
|
||||
- case: cross-region-fallback
|
||||
condition: always
|
||||
keeps: precise
|
||||
source_ref: "libretro/pcsx_rearmed@c8816799 libpcsxcore/psxmem.c:291-327"
|
||||
|
||||
sega-megacd:
|
||||
genesis_plus_gx:
|
||||
default_region: north-america
|
||||
fallback_across_regions: false
|
||||
source_ref: "libretro/Genesis-Plus-GX@60bee4c7 core/loadrom.c:1056-1078, core/loadrom.c:394-418, core/loadrom.c:724-734, core/cd_hw/cdd.c:556-561"
|
||||
keep_all_regions:
|
||||
- case: user-region-option
|
||||
condition: option
|
||||
option: genesis_plus_gx_region_detect
|
||||
default: auto
|
||||
keeps: all-regions
|
||||
source_ref: "libretro/Genesis-Plus-GX@60bee4c7 libretro/libretro_core_options.h:120-134, libretro/libretro.c:1574-1585, core/loadrom.c:1160-1164"
|
||||
genesis_plus_gx_wide:
|
||||
default_region: north-america
|
||||
fallback_across_regions: false
|
||||
source_ref: "libretro/Genesis-Plus-GX-Wide@b7ad0054 core/loadrom.c:393-414, core/loadrom.c:1050-1070"
|
||||
genesis_plus_gx_expanded:
|
||||
default_region: north-america
|
||||
fallback_across_regions: false
|
||||
source_ref: "BillyTimeGames/Genesis-Plus-GX-Expanded-Rom-Size@dea3528c core/loadrom.c:393-414, core/loadrom.c:1048-1068"
|
||||
picodrive:
|
||||
default_region: north-america
|
||||
fallback_across_regions: false
|
||||
source_ref: "libretro/picodrive@1890c293 pico/media.c:185-258, pico/media.c:288-312, platform/libretro/libretro.c:1273-1335"
|
||||
blastem:
|
||||
preference: [north-america, europe, japan]
|
||||
fallback_across_regions: false
|
||||
source_ref: "libretro/blastem@ab743653 genesis.c:3245-3268, segacd.c:1573-1593, romdb.c:175-216, libblastem.c:501-503"
|
||||
keep_all_regions:
|
||||
- case: megacd-region-list
|
||||
condition: option
|
||||
option: blastem_region
|
||||
default: U
|
||||
keeps: listed-areas
|
||||
source_ref: "libretro/blastem@ab743653 segacd.c:1573-1593, libblastem.c:85-90, libblastem.c:195-198"
|
||||
|
||||
mega-cd:
|
||||
ares:
|
||||
preference: [north-america]
|
||||
source_ref: "ares-emulator/ares@d400eee9 mia/medium/mega-cd.cpp:40-58, desktop-ui/emulator/mega-cd.cpp:17-19, desktop-ui/emulator/mega-cd.cpp:70-81, desktop-ui/emulator/emulator.cpp:86-106"
|
||||
keep_all_regions:
|
||||
- case: multi-area-disc
|
||||
condition: disc
|
||||
when: the security code matches none of the three known CRC32s, so the disc lists every region
|
||||
keeps: listed-areas
|
||||
source_ref: "ares-emulator/ares@d400eee9 mia/medium/mega-cd.cpp:40-58, desktop-ui/settings/settings.hpp:71"
|
||||
|
||||
sega-mastersystem:
|
||||
genesis_plus_gx:
|
||||
default_region: north-america
|
||||
source_ref: "libretro/Genesis-Plus-GX@60bee4c7 core/cart_hw/sms_cart.c:831-874, core/loadrom.c:300-347, core/loadrom.c:498-520"
|
||||
keep_all_regions:
|
||||
- case: sms-crc-list
|
||||
condition: always
|
||||
keeps: all-regions
|
||||
source_ref: "libretro/Genesis-Plus-GX@60bee4c7 core/cart_hw/sms_cart.c:160-264, core/cart_hw/sms_cart.c:831-851"
|
||||
- case: user-region-option
|
||||
condition: option
|
||||
option: genesis_plus_gx_region_detect
|
||||
default: auto
|
||||
keeps: all-regions
|
||||
source_ref: "libretro/Genesis-Plus-GX@60bee4c7 libretro/libretro_core_options.h:120-162, core/cart_hw/sms_cart.c:618-623"
|
||||
|
||||
master-system:
|
||||
ares:
|
||||
source_ref: "ares-emulator/ares@d400eee9 mia/medium/master-system.cpp:50-104, desktop-ui/emulator/master-system.cpp:14-16, desktop-ui/emulator/master-system.cpp:100-107"
|
||||
|
||||
sega-saturn:
|
||||
beetle_saturn:
|
||||
preference: [north-america, japan, south-korea, europe, brazil, asia-ntsc, asia-pal, latin-america]
|
||||
source_ref: "libretro/beetle-saturn-libretro@30f4c120 disc.c:155-172, disc.c:686-734, mednafen/ss/ss.c:1846-1849"
|
||||
keep_all_regions:
|
||||
- case: saturn-disc-database
|
||||
condition: always
|
||||
keeps: all-regions
|
||||
source_ref: "libretro/beetle-saturn-libretro@30f4c120 mednafen/ss/db.c:36-55, mednafen/ss/db.c:1731-1745"
|
||||
- case: user-region-option
|
||||
condition: option
|
||||
option: beetle_saturn_region
|
||||
default: Auto Detect
|
||||
keeps: all-regions
|
||||
source_ref: "libretro/beetle-saturn-libretro@30f4c120 libretro_core_options.h:79-97, libretro.c:454-475"
|
||||
mednafen_stv:
|
||||
source_ref: "beudbeud/libretro-stv@e6571347 mednafen/ss/ss.cpp:1532-1537, mednafen/ss/ss.cpp:2599-2600, mednafen/ss/ss.cpp:1596-1630, mednafen/ss/db.cpp:48"
|
||||
keep_all_regions:
|
||||
- case: user-region-option
|
||||
condition: option
|
||||
option: mednafen_stv_region
|
||||
default: auto
|
||||
keeps: all-regions
|
||||
source_ref: "beudbeud/libretro-stv@e6571347 libretro/libretro.cpp:428-437, libretro/libretro_core_options.h:36-37"
|
||||
ymir:
|
||||
fallback_across_regions: true
|
||||
source_ref: "ymir-emu/Ymir@fe439e98 libs/ymir-core/src/ymir/media/saturn_header.cpp:29-40, libs/ymir-core/src/ymir/sys/saturn.cpp:308-350, apps/ymir-sdl3/src/app/services/rom_service.cpp:127-200"
|
||||
keep_all_regions:
|
||||
- case: multi-area-disc
|
||||
condition: disc
|
||||
when: the disc lists more than one area, picked by a user-configurable order
|
||||
keeps: listed-areas
|
||||
source_ref: "ymir-emu/Ymir@fe439e98 apps/ymir-sdl3/src/app/settings.cpp:1052-1055, libs/ymir-core/src/ymir/sys/saturn.cpp:308-350"
|
||||
- case: saturn-kr-ipl
|
||||
condition: disc
|
||||
when: area T (Asia NTSC) or K (Korea)
|
||||
keeps: all-regions
|
||||
source_ref: "ymir-emu/Ymir@fe439e98 apps/ymir-sdl3/src/app/services/rom_service.cpp:127-200"
|
||||
kronos:
|
||||
region_free: [saturn_bios.bin, sega_101.bin, mpr-17933.bin]
|
||||
source_ref: "libretro/yabause@3791ffb2 yabause/src/libretro/libretro.c:1618-1636"
|
||||
|
||||
sega-stv:
|
||||
beetle_saturn:
|
||||
source_ref: "libretro/beetle-saturn-libretro@30f4c120 libretro.c:1175-1195, libretro.c:1230-1255, mednafen/ss/ss.c:1830-1845"
|
||||
keep_all_regions:
|
||||
- case: saturn-disc-database
|
||||
condition: always
|
||||
keeps: all-regions
|
||||
source_ref: "libretro/beetle-saturn-libretro@30f4c120 mednafen/ss/db.c:382-1640, libretro.c:1175-1195"
|
||||
mednafen_stv:
|
||||
source_ref: "beudbeud/libretro-stv@e6571347 mednafen/ss/ss.cpp:1800-1820, mednafen/ss/ss.cpp:1521-1531, mednafen/ss/ss.cpp:2602-2605"
|
||||
keep_all_regions:
|
||||
- case: saturn-disc-database
|
||||
condition: always
|
||||
keeps: all-regions
|
||||
source_ref: "beudbeud/libretro-stv@e6571347 mednafen/ss/ss.cpp:1800-1820, mednafen/ss/db.cpp:169-1633"
|
||||
- case: user-region-option
|
||||
condition: option
|
||||
option: mednafen_stv_region
|
||||
default: auto
|
||||
keeps: all-regions
|
||||
source_ref: "beudbeud/libretro-stv@e6571347 libretro/libretro.cpp:428-437"
|
||||
kronos:
|
||||
region_free: [stvbios.zip]
|
||||
source_ref: "libretro/yabause@3791ffb2 yabause/src/ctrl/src/stv.c:112-136, yabause/src/ctrl/src/stv.c:2560-2570, yabause/src/libretro/libretro.c:1039-1051"
|
||||
|
||||
nintendo-gamecube:
|
||||
dolphin:
|
||||
source_ref: "libretro/dolphin@3aac3f05 Source/Core/DiscIO/VolumeGC.cpp:91-109, Source/Core/DiscIO/Enums.h:45-53, Source/Core/Core/Config/MainSettings.cpp:721-768"
|
||||
keep_all_regions:
|
||||
- case: gamecube-unknown-region
|
||||
condition: disc
|
||||
when: the u32 at disc offset 0x458 is not 0, 1, 2 or 4
|
||||
keeps: all-regions
|
||||
source_ref: "libretro/dolphin@3aac3f05 Source/Core/Core/ConfigManager.cpp:489-490, Source/Core/Core/Config/DefaultLocale.cpp:148-191, Source/Core/Core/HW/EXI/EXI_DeviceIPL.cpp:100-131"
|
||||
primehack:
|
||||
source_ref: "shiiion/dolphin@53f53e0f Source/Core/DiscIO/VolumeGC.cpp:108, Source/Core/Core/ConfigManager.cpp:458"
|
||||
dolphin-mmjr2:
|
||||
source_ref: "nachoverdon/Dolphin-MMJR2@4d0fdb8e Source/Core/DiscIO/VolumeGC.cpp:82, Source/Core/Core/ConfigManager.cpp:337"
|
||||
triforce:
|
||||
source_ref: "REG-Linux/dolphin-triforce@5c456e8f Source/Core/DiscIO/VolumeGC.cpp:89, Source/Core/Core/ConfigManager.cpp:355"
|
||||
|
||||
nintendo-64:
|
||||
simple64:
|
||||
default_region: north-america
|
||||
source_ref: "simple64/simple64@d8c969c7 mupen64plus-core/src/main/rom.c:190, mupen64plus-core/src/main/rom.c:502-526, mupen64plus-core/src/main/main.c:1565-1580"
|
||||
keep_all_regions:
|
||||
- case: optional-pif
|
||||
condition: always
|
||||
keeps: all-regions
|
||||
source_ref: "simple64/simple64@d8c969c7 mupen64plus-core/src/main/main.c:1624-1633"
|
||||
|
||||
nintendo-64dd:
|
||||
ares:
|
||||
default_region: north-america
|
||||
source_ref: "ares-emulator/ares@d400eee9 mia/medium/nintendo-64dd.cpp:57-77, desktop-ui/emulator/nintendo-64dd.cpp:17-19, desktop-ui/emulator/nintendo-64dd.cpp:64-80"
|
||||
project64:
|
||||
source_ref: "project64/project64@24376e86 Source/Project64-core/N64System/N64Disk.cpp:828-840, Source/Project64-core/N64System/N64System.cpp:641-662"
|
||||
rosalies-mupen-gui:
|
||||
source_ref: "Rosalie241/RMG@4777ed3f Source/3rdParty/mupen64plus-core/src/device/dd/disk.c:405-480, Source/RMG-Core/MediaLoader.cpp:47-63"
|
||||
mupen64plus_next:
|
||||
region_free: [IPL.n64]
|
||||
source_ref: "libretro/mupen64plus-libretro-nx@15bada85 mupen64plus-core/src/main/main.c:953-973"
|
||||
parallel_n64:
|
||||
region_free: [64DD_IPL.bin]
|
||||
source_ref: "libretro/parallel-n64@f87550d9 libretro/libretro.c:951"
|
||||
|
||||
laseractive:
|
||||
geargrafx:
|
||||
source_ref: "drhelius/Geargrafx@7c447b0b platforms/libretro/libretro.cpp:830-850, platforms/libretro/libretro.cpp:513-529"
|
||||
keep_all_regions:
|
||||
- case: user-region-option
|
||||
condition: option
|
||||
option: geargrafx_laseractive_region
|
||||
default: Auto
|
||||
keeps: all-regions
|
||||
source_ref: "drhelius/Geargrafx@7c447b0b platforms/libretro/libretro.cpp:830-850"
|
||||
|
||||
sega-naomi:
|
||||
flycast:
|
||||
source_ref: "flyinghead/flycast@e88b0ced core/hw/naomi/naomi_cart.cpp:61-133"
|
||||
keep_all_regions:
|
||||
- case: naomi-one-archive
|
||||
condition: always
|
||||
keeps: one-archive
|
||||
source_ref: "flyinghead/flycast@e88b0ced core/hw/naomi/naomi_cart.cpp:61-133"
|
||||
|
||||
nintendo-ds:
|
||||
melonds_ds:
|
||||
source_ref: "JesseTG/melonds-ds@f394adba src/libretro/config/console.cpp:238-256, src/libretro/config/console.cpp:379-424"
|
||||
keep_all_regions:
|
||||
- case: nds-header-decides
|
||||
condition: always
|
||||
keeps: by-header
|
||||
source_ref: "JesseTG/melonds-ds@f394adba src/libretro/config/console.cpp:238-256, src/libretro/config/console.cpp:419-424"
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://abdess.github.io/retrobios/api/v1/schemas/game-extras.schema.json",
|
||||
"title": "RetroBIOS per-game extra files",
|
||||
"description": "games/extras.yml: files a game needs beyond its system's region BIOS, decided by the game's extension, header bytes or presence, with the core that reads them. A header trigger names a reader of games/headers.yml and the labels or values it returns.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["extras"],
|
||||
"properties": {
|
||||
"extras": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["system", "profile", "trigger", "required", "source_ref"],
|
||||
"properties": {
|
||||
"system": { "type": "string", "minLength": 1 },
|
||||
"profile": { "type": "string", "minLength": 1 },
|
||||
"trigger": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"minProperties": 1,
|
||||
"maxProperties": 1,
|
||||
"properties": {
|
||||
"extensions": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": { "type": "string", "pattern": "^\\.[a-z0-9]+$" },
|
||||
"uniqueItems": true
|
||||
},
|
||||
"header": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["reader", "values"],
|
||||
"properties": {
|
||||
"reader": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" },
|
||||
"field": { "type": "string", "pattern": "^[a-z0-9_]+$", "description": "A field of the reader, when the value is read there rather than from the reader's own location." },
|
||||
"values": { "type": "array", "minItems": 1, "items": { "type": ["string", "integer"] } }
|
||||
}
|
||||
},
|
||||
"every_game": { "const": true }
|
||||
}
|
||||
},
|
||||
"files": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 }, "uniqueItems": true },
|
||||
"region_files": { "const": true, "description": "The files are the system's region files for the game's region (games/regions.yml)." },
|
||||
"one_of": { "type": "boolean", "description": "Any single file of the list serves." },
|
||||
"filename_override": { "type": "boolean", "description": "Tags in the file name can change the choice: the installer keeps every file listed for this system and core." },
|
||||
"required": { "type": "boolean" },
|
||||
"note": { "type": "string", "minLength": 1 },
|
||||
"source_ref": {
|
||||
"type": "string",
|
||||
"pattern": "^[\\w.-]+/[\\w.-]+@[0-9a-f]{8,40} \\S.*$"
|
||||
}
|
||||
},
|
||||
"oneOf": [{ "required": ["files"] }, { "required": ["region_files"] }]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://abdess.github.io/retrobios/api/v1/schemas/game-regions.schema.json",
|
||||
"title": "RetroBIOS region rules per system and core",
|
||||
"description": "games/regions.yml: what a core does around its region files (routes, default region, preference order, region-free files, cross-region fallback) and the cases where a proven game region does not settle the file. Files per region come from the profiles' region: fields, never from here.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["cases", "systems"],
|
||||
"$defs": {
|
||||
"region": { "type": "string", "pattern": "^[a-z]+(?:-[a-z]+)*$" },
|
||||
"ref": {
|
||||
"type": "string",
|
||||
"description": "'owner/repo@sha path:line[, path:line]', several repositories joined by '; '. The sha is a prefix of the profile's source_commit or upstream_commit.",
|
||||
"pattern": "^[\\w.-]+/[\\w.-]+@[0-9a-f]{8,40} \\S.*$"
|
||||
},
|
||||
"keep": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["case", "condition", "keeps", "source_ref"],
|
||||
"properties": {
|
||||
"case": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" },
|
||||
"condition": {
|
||||
"enum": ["always", "option", "disc", "user-state"],
|
||||
"description": "always: whatever the game. option: when a core option leaves its default. disc: for the games 'when' describes. user-state: per-game state the user set on the device."
|
||||
},
|
||||
"option": { "type": "string", "minLength": 1 },
|
||||
"default": { "type": "string", "minLength": 1 },
|
||||
"when": { "type": "string", "minLength": 1 },
|
||||
"keeps": {
|
||||
"enum": ["all-regions", "listed-areas", "region-free-files", "first-disc", "precise", "by-header", "one-archive"],
|
||||
"description": "all-regions: every region file of the system. listed-areas: every region the disc lists. region-free-files: the files named, never dropped. first-disc: read disc 1 of a multi-disc set. precise: the precise file is enough, the game still starts without it. by-header: decided by header bytes (games/extras.yml). one-archive: one archive serves every region."
|
||||
},
|
||||
"files": { "type": "array", "items": { "type": "string", "minLength": 1 }, "minItems": 1 },
|
||||
"source_ref": { "$ref": "#/$defs/ref" }
|
||||
},
|
||||
"allOf": [
|
||||
{ "if": { "properties": { "condition": { "const": "option" } } }, "then": { "required": ["option", "default"] } },
|
||||
{ "if": { "properties": { "condition": { "enum": ["disc", "user-state"] } } }, "then": { "required": ["when"] } },
|
||||
{ "if": { "properties": { "keeps": { "const": "region-free-files" } } }, "then": { "required": ["files"] } }
|
||||
]
|
||||
},
|
||||
"core": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["source_ref"],
|
||||
"properties": {
|
||||
"routes": {
|
||||
"type": "object",
|
||||
"description": "Game region the core computes -> the region whose files it opens, where the profile's tags do not already say so.",
|
||||
"propertyNames": { "$ref": "#/$defs/region" },
|
||||
"additionalProperties": { "$ref": "#/$defs/region" }
|
||||
},
|
||||
"default_region": { "$ref": "#/$defs/region" },
|
||||
"preference": { "type": "array", "items": { "$ref": "#/$defs/region" }, "uniqueItems": true, "minItems": 1 },
|
||||
"region_free": { "type": "array", "items": { "type": "string", "minLength": 1 }, "uniqueItems": true },
|
||||
"fallback_across_regions": { "type": "boolean" },
|
||||
"source_ref": { "$ref": "#/$defs/ref" },
|
||||
"keep_all_regions": { "type": "array", "items": { "$ref": "#/$defs/keep" } }
|
||||
}
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"cases": {
|
||||
"type": "object",
|
||||
"propertyNames": { "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" },
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["title", "reason"],
|
||||
"properties": {
|
||||
"title": { "type": "string", "minLength": 1 },
|
||||
"reason": { "type": "string", "minLength": 1 }
|
||||
}
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"minProperties": 1,
|
||||
"additionalProperties": { "$ref": "#/$defs/core" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
"""Load and check games/regions.yml and games/extras.yml against the profiles.
|
||||
|
||||
python scripts/game_rules.py # check, print a summary
|
||||
python scripts/game_rules.py --json # the summary as JSON
|
||||
|
||||
Both files name profiles and the files those profiles declare. A rule is
|
||||
only as good as that link: a profile renamed, a file dropped from it, a
|
||||
region slug the vocabulary does not know, a citation at a revision the
|
||||
profile is not pinned to, or a header trigger asking a reader of
|
||||
games/headers.yml for a value it never returns, and the installer would act
|
||||
on a rule nothing backs. Each of those is an error here.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from common import load_emulator_profiles
|
||||
from region import REGIONS, normalize_declared
|
||||
from safeparse import yaml_load
|
||||
|
||||
REGIONS_FILE = Path("games") / "regions.yml"
|
||||
EXTRAS_FILE = Path("games") / "extras.yml"
|
||||
HEADERS_FILE = Path("games") / "headers.yml"
|
||||
|
||||
_REF_REPO = re.compile(r"(?:^|; )([\w.-]+/[\w.-]+)@([0-9a-f]{8,40}) ")
|
||||
|
||||
|
||||
def load_yaml(path: Path) -> dict:
|
||||
with Path(path).open(encoding="utf-8") as handle:
|
||||
return yaml_load(handle) or {}
|
||||
|
||||
|
||||
def profile_pins(profile: dict) -> set[str]:
|
||||
"""Every commit a profile is pinned to, a build-mode dict included."""
|
||||
pins: set[str] = set()
|
||||
for key in ("source_commit", "upstream_commit"):
|
||||
value = profile.get(key)
|
||||
values = value.values() if isinstance(value, dict) else [value]
|
||||
pins.update(str(v) for v in values if v)
|
||||
return pins
|
||||
|
||||
|
||||
def declared_names(profile: dict) -> set[str]:
|
||||
"""The names and aliases a profile's files answer to."""
|
||||
names: set[str] = set()
|
||||
for entry in profile.get("files") or []:
|
||||
if isinstance(entry, dict) and entry.get("name"):
|
||||
names.add(str(entry["name"]))
|
||||
names.update(str(a) for a in entry.get("aliases") or [])
|
||||
return names
|
||||
|
||||
|
||||
def tagged_regions(profile: dict, name: str) -> set[str]:
|
||||
"""The regions a profile tags a file with, over all its declarations."""
|
||||
out: set[str] = set()
|
||||
for entry in profile.get("files") or []:
|
||||
if isinstance(entry, dict) and (
|
||||
entry.get("name") == name or name in (entry.get("aliases") or [])
|
||||
):
|
||||
out |= normalize_declared(entry.get("region"))
|
||||
return out
|
||||
|
||||
|
||||
def every_tag(profile: dict) -> set[str]:
|
||||
return {
|
||||
region
|
||||
for entry in profile.get("files") or []
|
||||
if isinstance(entry, dict)
|
||||
for region in normalize_declared(entry.get("region"))
|
||||
}
|
||||
|
||||
|
||||
def header_readers(headers: dict) -> dict[str, dict]:
|
||||
"""Every reader of games/headers.yml by id, over all its systems."""
|
||||
readers: dict[str, dict] = {}
|
||||
for system in (headers.get("systems") or {}).values():
|
||||
readers.update(system.get("readers") or {})
|
||||
return readers
|
||||
|
||||
|
||||
def reader_answers(reader: dict, field: str | None = None) -> set[object] | None:
|
||||
"""The labels and values a reader can return, None when any string can
|
||||
be (a serial or product id read as it stands)."""
|
||||
if field:
|
||||
spec = (reader.get("fields") or {}).get(field) or {}
|
||||
return {v for k, v in spec.items() if k not in ("encoding",) and isinstance(v, str)}
|
||||
decode = reader.get("decode") or {}
|
||||
if decode.get("kind") == "serial":
|
||||
return None
|
||||
answers: set[object] = set()
|
||||
for row in decode.get("values") or []:
|
||||
answers.update(row[k] for k in ("value", "label") if k in row)
|
||||
for key in ("then", "otherwise"):
|
||||
branch = decode.get(key)
|
||||
if isinstance(branch, dict) and "label" in branch:
|
||||
answers.add(branch["label"])
|
||||
elif isinstance(branch, str):
|
||||
answers.add(branch)
|
||||
return answers
|
||||
|
||||
|
||||
@dataclass
|
||||
class Checker:
|
||||
profiles: dict[str, dict]
|
||||
readers: dict[str, dict] | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.errors: list[str] = []
|
||||
|
||||
def _profile(self, where: str, name: str, system: str) -> dict | None:
|
||||
profile = self.profiles.get(name)
|
||||
if profile is None:
|
||||
self.errors.append(f"{where}: no profile {name}")
|
||||
return None
|
||||
if system not in (profile.get("systems") or []):
|
||||
self.errors.append(f"{where}: {name} does not declare system {system}")
|
||||
return profile
|
||||
|
||||
def _files(self, where: str, name: str, profile: dict, files: list[str]) -> None:
|
||||
known = declared_names(profile)
|
||||
for file in files:
|
||||
if file not in known:
|
||||
self.errors.append(f"{where}: {name} declares no file {file}")
|
||||
|
||||
def _region(self, where: str, value: str) -> None:
|
||||
if value not in REGIONS:
|
||||
self.errors.append(f"{where}: unknown region {value}")
|
||||
|
||||
def _ref(self, where: str, profile: dict, ref: str | None) -> None:
|
||||
if not ref:
|
||||
self.errors.append(f"{where}: no source_ref")
|
||||
return
|
||||
pins = profile_pins(profile)
|
||||
cited = _REF_REPO.findall(ref)
|
||||
if not cited:
|
||||
self.errors.append(f"{where}: source_ref names no owner/repo@sha")
|
||||
for repo, sha in cited:
|
||||
if not any(pin.startswith(sha) for pin in pins):
|
||||
self.errors.append(f"{where}: {repo}@{sha} is not a pin of the profile")
|
||||
|
||||
def regions(self, doc: dict) -> None:
|
||||
cases = doc.get("cases") or {}
|
||||
for system, cores in (doc.get("systems") or {}).items():
|
||||
for name, rule in cores.items():
|
||||
where = f"regions {system}/{name}"
|
||||
profile = self._profile(where, name, system)
|
||||
if profile is None:
|
||||
continue
|
||||
self._ref(where, profile, rule.get("source_ref"))
|
||||
tags = every_tag(profile)
|
||||
for src, dst in (rule.get("routes") or {}).items():
|
||||
self._region(where, src)
|
||||
self._region(where, dst)
|
||||
if dst not in tags:
|
||||
self.errors.append(f"{where}: route {src} -> {dst}, but no file is tagged {dst}")
|
||||
for value in [rule.get("default_region"), *(rule.get("preference") or [])]:
|
||||
if value:
|
||||
self._region(where, value)
|
||||
free = rule.get("region_free") or []
|
||||
self._files(where, name, profile, free)
|
||||
for file in free:
|
||||
specific = tagged_regions(profile, file) - {"world"}
|
||||
if specific:
|
||||
self.errors.append(
|
||||
f"{where}: region_free {file} is tagged {sorted(specific)} in the profile"
|
||||
)
|
||||
for keep in rule.get("keep_all_regions") or []:
|
||||
kwhere = f"{where} case {keep.get('case')}"
|
||||
if keep.get("case") not in cases:
|
||||
self.errors.append(f"{kwhere}: case not described under cases")
|
||||
self._ref(kwhere, profile, keep.get("source_ref"))
|
||||
self._files(kwhere, name, profile, keep.get("files") or [])
|
||||
|
||||
def extras(self, doc: dict) -> None:
|
||||
for index, entry in enumerate(doc.get("extras") or []):
|
||||
where = f"extras[{index}] {entry.get('system')}/{entry.get('profile')}"
|
||||
profile = self._profile(where, entry["profile"], entry["system"])
|
||||
if profile is None:
|
||||
continue
|
||||
self._ref(where, profile, entry.get("source_ref"))
|
||||
self._files(where, entry["profile"], profile, entry.get("files") or [])
|
||||
header = (entry.get("trigger") or {}).get("header")
|
||||
if header and self.readers is not None:
|
||||
self._header(where, header)
|
||||
|
||||
def _header(self, where: str, header: dict) -> None:
|
||||
reader = self.readers.get(header["reader"])
|
||||
if reader is None:
|
||||
self.errors.append(f"{where}: games/headers.yml has no reader {header['reader']}")
|
||||
return
|
||||
field = header.get("field")
|
||||
if field and field not in (reader.get("fields") or {}):
|
||||
self.errors.append(f"{where}: reader {header['reader']} has no field {field}")
|
||||
return
|
||||
answers = reader_answers(reader, field)
|
||||
if answers is None:
|
||||
return
|
||||
for value in header.get("values") or []:
|
||||
if value not in answers:
|
||||
self.errors.append(
|
||||
f"{where}: reader {header['reader']} never returns {value!r}"
|
||||
)
|
||||
|
||||
|
||||
def check(
|
||||
regions: dict, extras: dict, profiles: dict[str, dict], headers: dict | None = None
|
||||
) -> list[str]:
|
||||
"""Errors over both files. Without headers, header triggers go unchecked."""
|
||||
checker = Checker(profiles, header_readers(headers) if headers is not None else None)
|
||||
checker.regions(regions)
|
||||
checker.extras(extras)
|
||||
return checker.errors
|
||||
|
||||
|
||||
def summary(regions: dict, extras: dict) -> dict:
|
||||
keeps = [
|
||||
{"case": keep["case"], "system": system, "profile": name, "condition": keep["condition"]}
|
||||
for system, cores in sorted((regions.get("systems") or {}).items())
|
||||
for name, rule in sorted(cores.items())
|
||||
for keep in rule.get("keep_all_regions") or []
|
||||
]
|
||||
systems = regions.get("systems") or {}
|
||||
return {
|
||||
"region_systems": len(systems),
|
||||
"region_cores": sum(len(c) for c in systems.values()),
|
||||
"cases": sorted(regions.get("cases") or {}),
|
||||
"case_entries": keeps,
|
||||
"extras": len(extras.get("extras") or []),
|
||||
"extra_systems": sorted({e["system"] for e in extras.get("extras") or []}),
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(allow_abbrev=False, description=__doc__.splitlines()[0])
|
||||
parser.add_argument("--regions", type=Path, default=REGIONS_FILE)
|
||||
parser.add_argument("--extras", type=Path, default=EXTRAS_FILE)
|
||||
parser.add_argument("--headers", type=Path, default=HEADERS_FILE)
|
||||
parser.add_argument("--emulators-dir", default="emulators")
|
||||
parser.add_argument("--json", action="store_true", help="print the summary as JSON")
|
||||
args = parser.parse_args()
|
||||
|
||||
regions = load_yaml(args.regions)
|
||||
extras = load_yaml(args.extras)
|
||||
errors = check(regions, extras, load_emulator_profiles(args.emulators_dir), load_yaml(args.headers))
|
||||
info = summary(regions, extras)
|
||||
if args.json:
|
||||
print(json.dumps({**info, "errors": errors}, indent=2))
|
||||
else:
|
||||
print(
|
||||
f"Region rules: {info['region_cores']} cores over {info['region_systems']} systems, "
|
||||
f"{len(info['cases'])} cases in {len(info['case_entries'])} entries"
|
||||
)
|
||||
for row in info["case_entries"]:
|
||||
print(f" {row['case']:26} {row['system']:20} {row['profile']:22} {row['condition']}")
|
||||
print(f"Per-game extras: {info['extras']} entries over {len(info['extra_systems'])} systems")
|
||||
for error in errors:
|
||||
print(f"ERROR {error}")
|
||||
return 1 if errors else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from common import run_cli
|
||||
|
||||
run_cli(main)
|
||||
@@ -0,0 +1,9 @@
|
||||
emulator: FDS Core
|
||||
type: libretro
|
||||
source: https://github.com/example/fdscore
|
||||
source_commit: 2222222222222222222222222222222222222222
|
||||
systems: [nintendo-nes, nintendo-fds]
|
||||
files:
|
||||
- name: disksys.rom
|
||||
system: nintendo-fds
|
||||
required: true
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
emulator: PSX Core
|
||||
type: libretro
|
||||
source: https://github.com/example/psxcore
|
||||
source_commit: 1111111111111111111111111111111111111111
|
||||
systems: [sony-playstation]
|
||||
files:
|
||||
- name: scph5500.bin
|
||||
region: [japan]
|
||||
required: false
|
||||
- name: scph5501.bin
|
||||
aliases: [scph7003.bin]
|
||||
region: [north-america]
|
||||
required: false
|
||||
- name: scph5502.bin
|
||||
region: [europe]
|
||||
required: false
|
||||
- name: openbios.bin
|
||||
required: false
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
extras:
|
||||
- system: nintendo-fds
|
||||
profile: fdscore
|
||||
trigger:
|
||||
extensions: [.fds]
|
||||
files: [disksys.rom]
|
||||
required: true
|
||||
source_ref: "example/fdscore@22222222 fds.c:5"
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
systems:
|
||||
nintendo-fds:
|
||||
readers:
|
||||
fds-image:
|
||||
location: {offset: 0, length: 14, encoding: ascii}
|
||||
decode:
|
||||
kind: exact-prefix
|
||||
values:
|
||||
- {value: "FDS\x1a", label: fds}
|
||||
otherwise: unknown
|
||||
sega-dreamcast:
|
||||
readers:
|
||||
dc-product-id:
|
||||
location: {offset: 64, length: 10, encoding: ascii}
|
||||
fields:
|
||||
disk_type: {offset: 37, length: 6, encoding: ascii, mil_cd: MIL CD}
|
||||
decode:
|
||||
kind: serial
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
cases:
|
||||
psx-override-bios:
|
||||
title: Region-free override BIOS
|
||||
reason: An option swaps in a region-free image for every region.
|
||||
systems:
|
||||
sony-playstation:
|
||||
psxcore:
|
||||
routes:
|
||||
south-korea: japan
|
||||
default_region: north-america
|
||||
region_free: [openbios.bin]
|
||||
source_ref: "example/psxcore@11111111 core.c:10-20"
|
||||
keep_all_regions:
|
||||
- case: psx-override-bios
|
||||
condition: option
|
||||
option: psxcore_override
|
||||
default: disabled
|
||||
keeps: region-free-files
|
||||
files: [openbios.bin]
|
||||
source_ref: "example/psxcore@11111111 core.c:30"
|
||||
@@ -0,0 +1,202 @@
|
||||
"""games/regions.yml and games/extras.yml: the rules the games-on-card mode reads.
|
||||
|
||||
Every rule names a profile and files that profile declares, cites the code
|
||||
at the profile's own pin, and speaks the region vocabulary of region.py.
|
||||
The checker is run on small fixtures, once clean and once per way a rule
|
||||
can lose its backing, then on the repository's own files.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
|
||||
import game_rules # noqa: E402
|
||||
from common import load_emulator_profiles # noqa: E402
|
||||
|
||||
FIXTURES = REPO_ROOT / "tests" / "fixtures" / "game_rules"
|
||||
|
||||
# The cases the research names where a proven game region does not settle
|
||||
# the file (games_on_card_research.md section 2).
|
||||
EXPECTED_CASES = {
|
||||
"user-region-option", "psx-override-bios", "duckstation-custom-region",
|
||||
"cross-region-fallback", "first-disc-region", "multi-area-disc",
|
||||
"saturn-kr-ipl", "saturn-disc-database", "sms-crc-list",
|
||||
"megacd-region-list", "gamecube-unknown-region", "naomi-one-archive",
|
||||
"nds-header-decides", "optional-pif",
|
||||
}
|
||||
|
||||
|
||||
def _schema_validator(name: str):
|
||||
try:
|
||||
from jsonschema import Draft202012Validator
|
||||
except ImportError as exc:
|
||||
raise unittest.SkipTest(f"schema check needs jsonschema: {exc}") from exc
|
||||
schema = json.loads((REPO_ROOT / "schemas" / name).read_text(encoding="utf-8"))
|
||||
Draft202012Validator.check_schema(schema)
|
||||
return Draft202012Validator(schema)
|
||||
|
||||
|
||||
class FixtureChecks(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.profiles = load_emulator_profiles(str(FIXTURES / "emulators"))
|
||||
self.regions = game_rules.load_yaml(FIXTURES / "regions.yml")
|
||||
self.extras = game_rules.load_yaml(FIXTURES / "extras.yml")
|
||||
|
||||
def errors(self, regions=None, extras=None) -> list[str]:
|
||||
return game_rules.check(
|
||||
regions if regions is not None else self.regions,
|
||||
extras if extras is not None else self.extras,
|
||||
self.profiles,
|
||||
)
|
||||
|
||||
def psx(self, regions: dict) -> dict:
|
||||
return regions["systems"]["sony-playstation"]["psxcore"]
|
||||
|
||||
def test_clean_fixture_passes(self) -> None:
|
||||
self.assertEqual(self.errors(), [])
|
||||
|
||||
def test_unknown_profile(self) -> None:
|
||||
extras = copy.deepcopy(self.extras)
|
||||
extras["extras"][0]["profile"] = "nocore"
|
||||
self.assertTrue(any("no profile nocore" in e for e in self.errors(extras=extras)))
|
||||
|
||||
def test_system_the_profile_does_not_run(self) -> None:
|
||||
extras = copy.deepcopy(self.extras)
|
||||
extras["extras"][0]["system"] = "sega-saturn"
|
||||
self.assertTrue(any("does not declare system" in e for e in self.errors(extras=extras)))
|
||||
|
||||
def test_undeclared_file(self) -> None:
|
||||
extras = copy.deepcopy(self.extras)
|
||||
extras["extras"][0]["files"] = ["FdsBios.bin"]
|
||||
self.assertTrue(any("declares no file FdsBios.bin" in e for e in self.errors(extras=extras)))
|
||||
|
||||
def test_alias_counts_as_declared(self) -> None:
|
||||
regions = copy.deepcopy(self.regions)
|
||||
self.psx(regions)["keep_all_regions"][0]["files"] = ["scph7003.bin"]
|
||||
self.assertFalse(any("declares no file" in e for e in self.errors(regions=regions)))
|
||||
|
||||
def test_unknown_region(self) -> None:
|
||||
regions = copy.deepcopy(self.regions)
|
||||
self.psx(regions)["default_region"] = "usa-canada"
|
||||
self.assertTrue(any("unknown region usa-canada" in e for e in self.errors(regions=regions)))
|
||||
|
||||
def test_route_to_a_region_no_file_carries(self) -> None:
|
||||
regions = copy.deepcopy(self.regions)
|
||||
self.psx(regions)["routes"] = {"south-korea": "oceania"}
|
||||
self.assertTrue(any("no file is tagged oceania" in e for e in self.errors(regions=regions)))
|
||||
|
||||
def test_citation_at_another_revision(self) -> None:
|
||||
regions = copy.deepcopy(self.regions)
|
||||
self.psx(regions)["source_ref"] = "example/psxcore@deadbeef core.c:10"
|
||||
self.assertTrue(any("not a pin of the profile" in e for e in self.errors(regions=regions)))
|
||||
|
||||
def test_missing_citation(self) -> None:
|
||||
regions = copy.deepcopy(self.regions)
|
||||
del self.psx(regions)["keep_all_regions"][0]["source_ref"]
|
||||
self.assertTrue(any("no source_ref" in e for e in self.errors(regions=regions)))
|
||||
|
||||
def test_case_without_description(self) -> None:
|
||||
regions = copy.deepcopy(self.regions)
|
||||
self.psx(regions)["keep_all_regions"][0]["case"] = "made-up"
|
||||
self.assertTrue(any("not described" in e for e in self.errors(regions=regions)))
|
||||
|
||||
def test_region_free_file_tagged_with_a_region(self) -> None:
|
||||
regions = copy.deepcopy(self.regions)
|
||||
self.psx(regions)["region_free"] = ["scph5500.bin"]
|
||||
self.assertTrue(any("tagged ['japan']" in e for e in self.errors(regions=regions)))
|
||||
|
||||
def header_errors(self, header: dict) -> list[str]:
|
||||
extras = copy.deepcopy(self.extras)
|
||||
extras["extras"][0]["trigger"] = {"header": header}
|
||||
headers = game_rules.load_yaml(FIXTURES / "headers.yml")
|
||||
return game_rules.check(self.regions, extras, self.profiles, headers)
|
||||
|
||||
def test_header_trigger_takes_a_label_the_reader_returns(self) -> None:
|
||||
self.assertEqual(self.header_errors({"reader": "fds-image", "values": ["fds"]}), [])
|
||||
|
||||
def test_header_trigger_with_free_text(self) -> None:
|
||||
errors = self.header_errors({"reader": "fds-image", "values": ["FDS header at 0"]})
|
||||
self.assertTrue(any("never returns 'FDS header at 0'" in e for e in errors))
|
||||
|
||||
def test_header_trigger_naming_no_reader(self) -> None:
|
||||
errors = self.header_errors({"reader": "fds-magic", "values": ["fds"]})
|
||||
self.assertTrue(any("no reader fds-magic" in e for e in errors))
|
||||
|
||||
def test_serial_reader_takes_any_value(self) -> None:
|
||||
self.assertEqual(self.header_errors({"reader": "dc-product-id", "values": ["T7001D 50"]}), [])
|
||||
|
||||
def test_header_field_value(self) -> None:
|
||||
self.assertEqual(
|
||||
self.header_errors({"reader": "dc-product-id", "field": "disk_type", "values": ["MIL CD"]}), [])
|
||||
errors = self.header_errors({"reader": "dc-product-id", "field": "area", "values": ["J"]})
|
||||
self.assertTrue(any("has no field area" in e for e in errors))
|
||||
|
||||
def test_summary_is_stable(self) -> None:
|
||||
first = game_rules.summary(self.regions, self.extras)
|
||||
second = game_rules.summary(copy.deepcopy(self.regions), copy.deepcopy(self.extras))
|
||||
self.assertEqual(json.dumps(first, sort_keys=True), json.dumps(second, sort_keys=True))
|
||||
self.assertEqual(first["case_entries"][0]["case"], "psx-override-bios")
|
||||
|
||||
|
||||
class FixtureSchemas(unittest.TestCase):
|
||||
def test_fixtures_match_the_schemas(self) -> None:
|
||||
for name, schema in (("regions.yml", "game-regions.schema.json"),
|
||||
("extras.yml", "game-extras.schema.json")):
|
||||
errors = list(_schema_validator(schema).iter_errors(
|
||||
game_rules.load_yaml(FIXTURES / name)))
|
||||
self.assertEqual(errors, [], name)
|
||||
|
||||
def test_schema_refuses_an_option_case_without_its_option(self) -> None:
|
||||
regions = game_rules.load_yaml(FIXTURES / "regions.yml")
|
||||
keep = regions["systems"]["sony-playstation"]["psxcore"]["keep_all_regions"][0]
|
||||
del keep["option"]
|
||||
self.assertTrue(list(_schema_validator("game-regions.schema.json").iter_errors(regions)))
|
||||
|
||||
def test_schema_refuses_two_triggers(self) -> None:
|
||||
extras = game_rules.load_yaml(FIXTURES / "extras.yml")
|
||||
extras["extras"][0]["trigger"]["every_game"] = True
|
||||
self.assertTrue(list(_schema_validator("game-extras.schema.json").iter_errors(extras)))
|
||||
|
||||
|
||||
class RepositoryRules(unittest.TestCase):
|
||||
"""The committed files, against the committed profiles."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
cls.regions = game_rules.load_yaml(REPO_ROOT / game_rules.REGIONS_FILE)
|
||||
cls.extras = game_rules.load_yaml(REPO_ROOT / game_rules.EXTRAS_FILE)
|
||||
|
||||
def test_every_rule_is_backed_by_its_profile(self) -> None:
|
||||
profiles = load_emulator_profiles(str(REPO_ROOT / "emulators"))
|
||||
headers = game_rules.load_yaml(REPO_ROOT / game_rules.HEADERS_FILE)
|
||||
self.assertEqual(game_rules.check(self.regions, self.extras, profiles, headers), [])
|
||||
|
||||
def test_files_match_the_schemas(self) -> None:
|
||||
self.assertEqual(list(_schema_validator("game-regions.schema.json").iter_errors(self.regions)), [])
|
||||
self.assertEqual(list(_schema_validator("game-extras.schema.json").iter_errors(self.extras)), [])
|
||||
|
||||
def test_the_fourteen_cases_are_encoded_and_used(self) -> None:
|
||||
self.assertEqual(set(self.regions["cases"]), EXPECTED_CASES)
|
||||
used = {row["case"] for row in game_rules.summary(self.regions, self.extras)["case_entries"]}
|
||||
self.assertEqual(used, EXPECTED_CASES)
|
||||
|
||||
def test_region_free_files_never_carry_a_region(self) -> None:
|
||||
# The installer keeps them for every region: a profile tag would let
|
||||
# a region filter drop what the code opens for every game.
|
||||
for system, cores in self.regions["systems"].items():
|
||||
for core, rule in cores.items():
|
||||
for keep in rule.get("keep_all_regions") or []:
|
||||
if keep["keeps"] == "region-free-files":
|
||||
self.assertTrue(set(keep["files"]) <= set(rule.get("region_free") or []),
|
||||
f"{system}/{core}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user