From 0543165ed26f2a688546c07d25fbd56cf11d930b Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Tue, 24 Mar 2026 22:31:22 +0100 Subject: [PATCH] feat: re-profile 22 emulators, refactor validation to common.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit batch re-profiled nekop2 through pokemini. mupen64plus renamed to mupen64plus_next. new profiles: nes, mupen64plus_next. validation functions (_build_validation_index, check_file_validation) consolidated in common.py — single source of truth for verify.py and generate_pack.py. pipeline 100% consistent on all 6 platforms. --- emulators/lrps2.yml | 125 ++++++-- .../{mupen64plus.yml => mupen64plus_next.yml} | 19 +- emulators/nekop2.yml | 153 ++++++++- emulators/nes.yml | 17 + emulators/nestopia.yml | 92 ++++-- emulators/np2kai.yml | 179 ++++++----- emulators/numero.yml | 71 ++--- emulators/nxengine.yml | 86 ++--- emulators/o2em.yml | 71 ++--- emulators/oberon.yml | 37 +-- emulators/onscripter.yml | 36 +-- emulators/onsyuri.yml | 19 +- emulators/openlara.yml | 31 +- emulators/opentyrian.yml | 4 +- emulators/opera.yml | 8 +- emulators/panda3ds.yml | 36 +-- emulators/parallel_n64.yml | 23 +- emulators/parallel_n64_debug.yml | 6 +- emulators/pascal_pong.yml | 11 +- emulators/pcem.yml | 298 ++++++++++++++++-- emulators/pcsx1.yml | 52 ++- emulators/pcsx_rearmed.yml | 51 +-- emulators/picodrive.yml | 78 +++-- emulators/pokemini.yml | 6 +- scripts/common.py | 252 ++++++++++++++- scripts/crypto_verify.py | 3 +- scripts/generate_pack.py | 113 +++---- scripts/refresh_data_dirs.py | 19 +- scripts/scraper/coreinfo_scraper.py | 1 + scripts/scraper/emudeck_scraper.py | 1 + scripts/validate_pr.py | 5 +- scripts/verify.py | 225 +------------ tests/test_e2e.py | 104 +++++- 33 files changed, 1449 insertions(+), 783 deletions(-) rename emulators/{mupen64plus.yml => mupen64plus_next.yml} (57%) create mode 100644 emulators/nes.yml diff --git a/emulators/lrps2.yml b/emulators/lrps2.yml index d84b4319..3666a854 100644 --- a/emulators/lrps2.yml +++ b/emulators/lrps2.yml @@ -1,30 +1,111 @@ -# LRPS2 — Sony PlayStation 2 (libretro) -# ref: libretro/lrps2, docs.libretro.com/library/lrps2 -# Same BIOS as PCSX2 standalone, placed in pcsx2/bios/ subfolder. -# Also needs GameIndex.yaml for game compatibility database. -# -# doc vs source: docs say "no specific filename required" for BIOS, any -# valid 4 MB PS2 BIOS dump works. Docs do not list GameIndex.yaml as a -# BIOS file, but the source code loads it from pcsx2/resources/ and it -# is needed for game-specific patches and compatibility fixes. -# SCPH-10000 not recommended per docs (lower compatibility). +# LRPS2 - libretro PlayStation 2 core +# Source: https://github.com/libretro/ps2 +# Upstream: https://github.com/PCSX2/pcsx2 + emulator: LRPS2 type: libretro +core_classification: community_fork +source: "https://github.com/libretro/ps2" +upstream: "https://github.com/PCSX2/pcsx2" +profiled_date: "2026-03-24" +core_version: "Git" +display_name: "Sony - PlayStation 2 (LRPS2)" cores: [lrps2] -source: "https://github.com/libretro/lrps2" -systems: - - sony-playstation-2 +systems: [sony-playstation-2] + +bios_directory: "pcsx2/bios/" +resources_directory: "pcsx2/resources/" notes: | - Libretro port of PCSX2. Uses the same BIOS files as standalone PCSX2. - BIOS must NOT be zipped. Any valid PS2 BIOS works (SCPH-10000 not - recommended due to lower compatibility). BIOS goes in pcsx2/bios/. - GameIndex.yaml goes in pcsx2/resources/ and is needed for proper - game-specific settings (patches, fixes). + Hard fork of PCSX2 ported to libretro. BIOS detection is filename-agnostic: the core + scans pcsx2/bios/ for any file between 4-8 MB with a valid romdir structure containing + RESET and ROMVER entries. The ROMVER entry determines region and version. + Companion files (.rom1, .rom2, .nvm, .mec) derive their paths from the selected BIOS. + DEV9 (network adapter) and USB are stubbed in the libretro port. + GameIndex.yaml in pcsx2/resources/ provides per-game patches for compatibility. files: - - name: GameIndex.yaml - system: sony-playstation-2 + - name: ".bin" + path: "pcsx2/bios/" required: true - note: "game compatibility database, not a BIOS — pcsx2/resources/GameIndex.yaml" - source_ref: "pcsx2/GameIndex.yaml" + description: "PS2 BIOS binary" + min_size: 4194304 + max_size: 8388608 + validation: [size] + source_ref: "pcsx2/ps2/BiosTools.cpp:230-254,266-322" + note: > + Scans pcsx2/bios/ for any file between 4 MB and 8 MB. Validates via romdir + structure parsing (RESET + ROMVER entries). User selects BIOS via core option + pcsx2_bios. Falls back to first valid BIOS found if configured BIOS is missing. + + - name: ".rom1" + path: "pcsx2/bios/" + required: false + max_size: 4194304 + description: "DVD player ROM" + source_ref: "pcsx2/ps2/BiosTools.cpp:189-210,313" + note: > + DVD player ROM. Tries .rom1 (appended) then .rom1 + (extension replaced). Silently skipped if not found. + + - name: ".rom2" + path: "pcsx2/bios/" + required: false + max_size: 4194304 + description: "Chinese ROM extension" + source_ref: "pcsx2/ps2/BiosTools.cpp:189-210,314" + note: > + Chinese region ROM extension. Same naming convention as rom1. + Only present on Chinese region consoles. + + - name: ".nvm" + path: "pcsx2/bios/" + required: false + hle_fallback: true + size: 1024 + description: "NVRAM / EEPROM data" + source_ref: "pcsx2/CDVD/CDVD.cpp:155-198" + note: > + Console EEPROM data (language, timezone, region, iLink ID, OSD settings). + Path derived from BIOS path with extension replaced to .nvm. + Auto-created with region-appropriate defaults if missing or invalid. + + - name: ".mec" + path: "pcsx2/bios/" + required: false + hle_fallback: true + size: 4 + description: "Mechacon version" + source_ref: "pcsx2/CDVD/CDVD.cpp:186-197" + note: > + Mechacon (disc drive controller) version as u32. + Auto-created with default 0x00020603 if missing. + + - name: "GameIndex.yaml" + path: "pcsx2/resources/GameIndex.yaml" + required: false + description: "game compatibility database" + source_ref: "pcsx2/GameDatabase.cpp:48,880" + note: > + YAML database of per-game patches, settings overrides, and compatibility fixes. + OSD warning shown if missing. Some games may not boot or have issues without it. + + - name: "cheats_ws.zip" + path: "pcsx2/resources/cheats_ws.zip" + required: false + description: "widescreen patches archive" + source_ref: "pcsx2/VMManager.cpp:340-353" + note: > + ZIP archive of per-game widescreen (16:9) patches in pnach format. + Only loaded when widescreen patches are enabled via core options. + Fallback if no patches found in pcsx2/cheats_ws/ folder. + + - name: "cheats_ni.zip" + path: "pcsx2/resources/cheats_ni.zip" + required: false + description: "no-interlacing patches archive" + source_ref: "pcsx2/VMManager.cpp:375-388" + note: > + ZIP archive of per-game no-interlacing patches in pnach format. + Only loaded when no-interlacing patches are enabled via core options. + Fallback if no patches found in pcsx2/cheats_ni/ folder. diff --git a/emulators/mupen64plus.yml b/emulators/mupen64plus_next.yml similarity index 57% rename from emulators/mupen64plus.yml rename to emulators/mupen64plus_next.yml index 2bbde454..0f1c4899 100644 --- a/emulators/mupen64plus.yml +++ b/emulators/mupen64plus_next.yml @@ -1,10 +1,10 @@ -emulator: Mupen64Plus-Next +emulator: "Mupen64Plus-Next" type: libretro core_classification: enhanced_fork source: "https://github.com/libretro/mupen64plus-libretro-nx" upstream: "https://github.com/mupen64plus/mupen64plus-core" profiled_date: "2026-03-24" -core_version: "2.8" +core_version: "2.6.0" display_name: "Nintendo - Nintendo 64 (Mupen64Plus-Next)" systems: [nintendo-64, nintendo-64dd] cores: [mupen64plus_next, mupen64plus_next_develop, mupen64plus_next_gles3, mupen64plus_next_gles2] @@ -14,8 +14,16 @@ files: path: "Mupen64plus/IPL.n64" size: 4194304 required: false - note: "64DD IPL ROM. Only needed for N64 Disk Drive games (.ndd) via subsystem API" - source_ref: "mupen64plus-core/src/main/main.c:954-979" + description: "64DD IPL ROM" + note: "Only needed for N64 Disk Drive games (.ndd) via subsystem API. Accepts Z64, N64, and V64 byte-swap formats." + source_ref: "mupen64plus-core/src/main/main.c:940-1024" + + - name: "font.ttf" + path: "Mupen64plus/font.ttf" + required: false + description: "TrueType font for on-screen display text" + note: "Fallback font for OSD text rendering (FPS counter, messages). System fonts tried first. No impact on emulation." + source_ref: "GLideN64/src/TextDrawer.cpp:165-170" notes: hle_available: true @@ -29,3 +37,6 @@ notes: transferpak_note: > Transfer Pak support (GB/GBC games on N64) handled via subsystem API. No additional firmware files needed. + embedded_data: > + mupen64plus.ini (ROM database) and GLideN64.custom.ini (per-game GPU + settings) are embedded as compiled headers and auto-generated at init. diff --git a/emulators/nekop2.yml b/emulators/nekop2.yml index cae5a9ad..8d1e246f 100644 --- a/emulators/nekop2.yml +++ b/emulators/nekop2.yml @@ -1,8 +1,149 @@ -emulator: "nekop2" -type: alias -alias_of: "np2kai" -profiled_date: "2026-03-18" +emulator: nekop2 +type: libretro +core_classification: community_fork +source: "https://github.com/libretro/libretro-meowPC98" +upstream: "https://np2.yui.ne.jp (dead; source preserved in libretro repo)" +profiled_date: "2026-03-24" core_version: "0.86" display_name: "NEC - PC-98 (Neko Project II)" -note: "This core uses the same BIOS/firmware as np2kai. See emulators/np2kai.yml for details." -files: [] +cores: [nekop2] +systems: [pc-98] + +# Neko Project II (NP2) by Yui, ported to libretro by meepingsnesroms. +# PC-9801/9821 emulator (80286/IA-32 CPU). All files load from /np2/. +# The libretro shim sets np2cfg.biospath to "/np2/" and +# file_setcd() to the same path (libretro/libretro.c:856-864,196). +# +# BIOS_SIMULATE is hardcoded (bios/bios.c:26), so the core always has +# a built-in BIOS simulator and ITF ROM is never loaded from disk. +# Font data falls back to built-in 8x8 tables. Sound ROM falls back +# to a 9-byte stub. IDE/SCSI/SASI all have built-in stubs. +# +# This is NOT the same core as np2kai. Different repo, different +# feature set (no fmgen, no IDE/PCI/GPIB ROM loading, no itf.rom). +# BIOS path is np2/, not np2kai/. +# +# .info declares firmware_count=0, which is misleading — the core +# loads multiple BIOS/font/sound files but all have built-in fallbacks. + +files: + # -- Main BIOS ROM -- + # Loaded in bios/bios.c:232-235 via getbiospath(). 96 KB (0x18000) + # mapped at 0xe8000. Without this, the built-in BIOS simulator + # (nosyscode + BIOS_SIMULATE) is used. + - name: "bios.rom" + path: "np2/bios.rom" + required: false + hle_fallback: true + note: > + PC-9801 system BIOS ROM (96 KB). The core boots without it using + the built-in BIOS simulator, but some software requires the real BIOS. + source_ref: "bios/bios.c:232-235, common/strres.c:53" + + # -- PC-9821 extension BIOS -- + # Loaded in bios/bios.c:262-266 via getbiospath(). 8 KB (0x2000) + # at 0xd8000. Only compiled when SUPPORT_PC9821 is defined + # (libretro/compiler.h:169). + - name: "bios9821.rom" + path: "np2/bios9821.rom" + required: false + note: > + PC-9821 extension BIOS ROM (8 KB). For PC-9821 mode support. + Mapped at 0xd8000. + source_ref: "bios/bios.c:262-266, libretro/compiler.h:169" + + # -- Font file -- + # Set in libretro.c:862 as "/np2/font.bmp". + # font_load() in font/font.c:113 determines type by extension: + # .bmp = PC98 format, FONT.ROM = V98 format. + # Without any font file, built-in fontdata_8 provides 8x8 ASCII only. + - name: "font.bmp" + path: "np2/font.bmp" + required: false + hle_fallback: true + aliases: ["FONT.ROM"] + note: > + PC-98 font bitmap (288 KB). Required for correct Japanese kanji display. + Without this, only basic ASCII renders using built-in 8x8 data. + FONT.ROM (V98 format) is also accepted. + source_ref: "libretro/libretro.c:862, font/font.c:113, font/fontdata.c:12" + + # -- Sound BIOS ROM -- + # Loaded by soundrom_load() via loadsoundrom() in sound/soundrom.c:21-55. + # Filename built as "sound" + optional board name + ".rom". + # Board-specific variants: sound26.rom (PC-9801-26K), sound86.rom (86), + # sound118.rom (118), soundSPB.rom (Speak Board), sound14.rom (14). + # The code tries the board-specific name first, falls back to sound.rom. + # 16 KB (0x4000). Fallback: 9-byte defsoundrom stub. + - name: "sound.rom" + path: "np2/sound.rom" + required: false + hle_fallback: true + note: > + FM sound board BIOS ROM (16 KB). The core tries board-specific + variants first (sound26.rom, sound86.rom, sound118.rom, soundSPB.rom, + sound14.rom) then falls back to sound.rom. + source_ref: "sound/soundrom.c:15-16,21-55,65-78" + + # -- YM2608 OPNA rhythm samples -- + # Loaded by rhythm_load() in sound/rhythmc.c:60-71 via getbiospath(). + # No fmgen engine in this core — only lowercase filenames are used. + - name: "2608_bd.wav" + path: "np2/2608_bd.wav" + required: false + note: "YM2608 OPNA rhythm sample: bass drum" + source_ref: "sound/rhythmc.c:11,60-71" + + - name: "2608_sd.wav" + path: "np2/2608_sd.wav" + required: false + note: "YM2608 OPNA rhythm sample: snare drum" + source_ref: "sound/rhythmc.c:12" + + - name: "2608_top.wav" + path: "np2/2608_top.wav" + required: false + note: "YM2608 OPNA rhythm sample: top cymbal" + source_ref: "sound/rhythmc.c:13" + + - name: "2608_hh.wav" + path: "np2/2608_hh.wav" + required: false + note: "YM2608 OPNA rhythm sample: hi-hat" + source_ref: "sound/rhythmc.c:14" + + - name: "2608_tom.wav" + path: "np2/2608_tom.wav" + required: false + note: "YM2608 OPNA rhythm sample: tom" + source_ref: "sound/rhythmc.c:15" + + - name: "2608_rim.wav" + path: "np2/2608_rim.wav" + required: false + note: "YM2608 OPNA rhythm sample: rim shot" + source_ref: "sound/rhythmc.c:16" + + # -- SCSI controller BIOS -- + # Loaded in cbus/scsiio.c:219 via file_open_rb_c(). 16 KB (0x4000). + # Falls back to built-in scsibios[] stub. + - name: "scsi.rom" + path: "np2/scsi.rom" + required: false + hle_fallback: true + note: > + SCSI controller BIOS ROM (16 KB). The core includes a built-in + SCSI BIOS stub as fallback. + source_ref: "cbus/scsiio.c:208-233" + + # -- SASI controller BIOS -- + # Loaded in cbus/sasiio.c:453 via file_open_rb_c(). 4 KB (0x1000). + # Falls back to built-in sasibios[] stub. + - name: "sasi.rom" + path: "np2/sasi.rom" + required: false + hle_fallback: true + note: > + SASI controller BIOS ROM (4 KB). The core includes a built-in + SASI BIOS stub as fallback. + source_ref: "cbus/sasiio.c:442-465" diff --git a/emulators/nes.yml b/emulators/nes.yml new file mode 100644 index 00000000..af793d78 --- /dev/null +++ b/emulators/nes.yml @@ -0,0 +1,17 @@ +emulator: nes +type: libretro +core_classification: community_fork +source: "https://github.com/rz5/nes" +upstream: "https://git.9front.org/plan9front/plan9front (sys/src/games/nes/)" +profiled_date: "2026-03-24" +core_version: "1.0" +display_name: "Nintendo - NES / Famicom (nes)" +cores: [nes] +systems: [nintendo-nes] +files: [] +notes: > + Port of aiju's NES emulator from 9front (Plan 9) to libretro by rz5. + Minimal emulator supporting mappers 0 (NROM), 1 (MMC1), 2 (UxROM), + 3 (CNROM), 4 (MMC3), 7 (AxROM). Palette hardcoded in ppu.c. + No BIOS or external files required. NES hardware boots directly + from cartridge ROM. No FDS support (only .nes extension). diff --git a/emulators/nestopia.yml b/emulators/nestopia.yml index 795a0289..89436c05 100644 --- a/emulators/nestopia.yml +++ b/emulators/nestopia.yml @@ -1,40 +1,36 @@ emulator: Nestopia UE type: libretro +core_classification: community_fork source: "https://github.com/libretro/nestopia" +upstream: "https://gitlab.com/jgemu/nestopia" logo: "https://raw.githubusercontent.com/0ldsk00l/nestopia/master/icons/svg/nestopia.svg" -profiled_date: "2026-03-18" +profiled_date: "2026-03-24" core_version: "1.53.1" display_name: "Nintendo - NES / Famicom (Nestopia)" +cores: [nestopia] systems: [nintendo-nes, nintendo-fds] notes: | - Nestopia UE (Undead Edition) is a cycle-accurate NES/Famicom emulator. - NES cartridge games need no BIOS. Famicom Disk System games require - the FDS BIOS ROM (disksys.rom, 8 KB) loaded from the system directory. - The core validates the BIOS via CRC32 against two known dumps: - standard Famicom (0x5E607DCF) and Twin Famicom (0x4DF24A6C). - An unknown BIOS triggers a warning but still loads. + Nestopia UE is a cycle-accurate NES/Famicom emulator. The libretro port + tracks the upstream Nestopia JG project. NES cartridge games need no BIOS. + Famicom Disk System games require disksys.rom (8 KB FDS BIOS) in the + system directory. The core validates the BIOS via CRC32 against two known + dumps: standard Famicom (0x5E607DCF) and Twin Famicom (0x4DF24A6C). An + unknown BIOS triggers a warning but still loads. - NstDatabase.xml is an optional game database used for region autodetection, - aspect ratio selection, and 4-player adapter recognition. A copy is baked - into the core binary (libretro/nstdatabase.hpp) and used as fallback when - no external file is found. Placing a newer NstDatabase.xml in the system - directory overrides the built-in copy. + NstDatabase.xml is an optional game database for region autodetection, + mapper selection, and 4-player adapter recognition. A copy is baked into + the core binary (libretro/nstdatabase.hpp) as fallback. The upstream + Nestopia JG requires this file; the libretro port makes it optional. - An optional custom palette file (custom.pal, 192 bytes, 64 RGB triplets) - overrides the built-in palettes when the nestopia_palette core option is - set to Custom. The core ships with 10+ built-in palettes (Royaltea, - Smooth FBx, etc.) so the external file is rarely needed. + An optional custom palette file (custom.pal, 64 RGB triplets) overrides + built-in palettes when the nestopia_palette core option is set to Custom. + The libretro port embeds all named palettes (Royaltea, Smooth FBx, etc.) + that the upstream loads from external .pal files. - Audio samples for specific games can be placed in system/nestopia/samples/ - as numbered .wav files (e.g., 00.wav, 01.wav). This is used for Famicom - expansion audio in a few titles. - - Game Genie is handled as cheat code decoding (software), not via a ROM file. - - File path construction: libretro/libretro.cpp retro_load_game() joins the - system directory (RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY) with each - filename directly - no subdirectories. + Five Famicom games use external ADPCM audio samples placed in + system/nestopia/samples/{game}/ as numbered .wav files. These originate + from MAME sample ZIPs. Games function without them but miss some audio. files: # --- Famicom Disk System BIOS (required for FDS games) --- @@ -43,6 +39,7 @@ files: description: "FDS BIOS ROM" required: true size: 8192 + validation: [crc32] md5: "ca30b50f880eb660a320674ed365ef7a" sha1: "57fe1bdee955bb48d357e463ccbf129496930b62" source_ref: "libretro/libretro.cpp:1608-1634 (FDS load), source/core/NstFds.cpp:61-131 (Bios class, CRC32 validation)" @@ -54,11 +51,8 @@ files: description: "Nestopia game database for region and mapper autodetection" required: false hle_fallback: true - size: 1022369 - md5: "0ee6cbdc6f5c96ce9c8aa5edb59066f4" - sha1: ~ source_ref: "libretro/libretro.cpp:1561-1586 (database load), libretro/nstdatabase.hpp (baked-in fallback)" - notes: "XML database matching games by SHA1+CRC32. Used for region, mapper, mirroring, and 4-player adapter detection. Built-in fallback exists so this file is optional. Hash is for the upstream copy shipped with the core repo." + notes: "XML database matching games by SHA1+CRC32. Used for region, mapper, mirroring, and 4-player adapter detection. Built-in fallback exists so this file is optional." # --- Custom palette (optional, core option nestopia_palette = Custom) --- - name: "custom.pal" @@ -67,7 +61,41 @@ files: required: false hle_fallback: true size: 192 - md5: ~ - sha1: ~ source_ref: "libretro/libretro.cpp:1540-1559 (palette load)" - notes: "64 RGB triplets (64 x 3 bytes = 192 bytes). Only loaded when nestopia_palette core option is set to Custom. Falls back to built-in Royaltea palette if not found. Multiple valid palettes exist so no single canonical hash." + notes: "64 RGB triplets (64 x 3 bytes = 192 bytes). Loaded unconditionally at startup; applied only when nestopia_palette core option is set to Custom. Falls back to built-in Royaltea palette. Multiple valid palettes exist so no canonical hash." + + # --- ADPCM audio samples (optional, game-specific) --- + - name: "nestopia/samples/moepro/" + system: nintendo-nes + description: "Moero Pro Yakyuu audio samples (16 .wav files, 00.wav-15.wav)" + required: false + category: game_data + source_ref: "libretro/libretro.cpp:142-180 (load_wav), libretro/libretro.cpp:247 (LOAD_SAMPLE_MOERO_PRO_YAKYUU callback), source/core/NstSoundPlayer.hpp:53 (16 samples)" + + - name: "nestopia/samples/moepro88/" + system: nintendo-nes + description: "Moero Pro Yakyuu '88 audio samples (20 .wav files, 00.wav-19.wav)" + required: false + category: game_data + source_ref: "libretro/libretro.cpp:142-180 (load_wav), libretro/libretro.cpp:249 (LOAD_SAMPLE_MOERO_PRO_YAKYUU_88 callback), source/core/NstSoundPlayer.hpp:54 (20 samples)" + + - name: "nestopia/samples/mptennis/" + system: nintendo-nes + description: "Moero Pro Tennis audio samples (19 .wav files, 00.wav-18.wav)" + required: false + category: game_data + source_ref: "libretro/libretro.cpp:142-180 (load_wav), libretro/libretro.cpp:251 (LOAD_SAMPLE_MOERO_PRO_TENNIS callback), source/core/NstSoundPlayer.hpp:55 (19 samples)" + + - name: "nestopia/samples/terao/" + system: nintendo-nes + description: "Terao no Dosukoi Oozumou audio samples (6 .wav files, 00.wav-05.wav)" + required: false + category: game_data + source_ref: "libretro/libretro.cpp:142-180 (load_wav), libretro/libretro.cpp:253 (LOAD_SAMPLE_TERAO_NO_DOSUKOI_OOZUMOU callback), source/core/NstSoundPlayer.hpp:56 (6 samples)" + + - name: "nestopia/samples/ftaerobi/" + system: nintendo-nes + description: "Aerobics Studio audio samples (8 .wav files, 00.wav-07.wav)" + required: false + category: game_data + source_ref: "libretro/libretro.cpp:142-180 (load_wav), libretro/libretro.cpp:255 (LOAD_SAMPLE_AEROBICS_STUDIO callback), source/core/NstSoundPlayer.hpp:57 (8 samples)" diff --git a/emulators/np2kai.yml b/emulators/np2kai.yml index 532aa43b..3dc5ced0 100644 --- a/emulators/np2kai.yml +++ b/emulators/np2kai.yml @@ -1,173 +1,204 @@ emulator: NP2kai type: libretro +core_classification: enhanced_fork source: "https://github.com/libretro/NP2kai" -profiled_date: "2026-03-18" +upstream: "https://github.com/AZO234/NP2kai" +profiled_date: "2026-03-24" core_version: "0.86" display_name: "NEC - PC-98 (Neko Project II Kai)" +cores: [np2kai] systems: [pc-98] -# NP2kai is a PC-9801/9821 emulator (Neko Project II kai). -# All BIOS/font/sound files are loaded from /np2kai/ subdirectory. -# The core sets np2cfg.biospath to "/np2kai/" in retro_load_game() +# NP2kai is a PC-9801/9821 emulator by AZO234, enhanced fork of Neko Project II +# by Yui (original upstream dead: np2.yui.ne.jp). All files load from +# /np2kai/ subdirectory, set in retro_load_game() # (sdl/libretro/libretro.c:1800-1815). All getbiospath() calls resolve # relative to that directory. # -# The core has a built-in BIOS simulator (BIOS_SIMULATE) that can boot -# without a real BIOS ROM, but a real bios.rom provides better compatibility. -# Font data is auto-generated from built-in tables if font.bmp is missing, -# but Japanese kanji display requires the real font file. +# BIOS_SIMULATE is unconditionally #define'd (bios/bios.c:78), so the core +# always has a built-in BIOS simulator. itf.rom loading (bios.c:569) is dead +# code behind #else of this define and is never executed. +# +# .info declares firmware_count=11 but is incomplete: misses IDE, SCSI, SASI, +# PCI, GPIB ROMs and key.txt. Lists itf.rom which is dead code (phantom). # # The fmgen YM2608 rhythm engine (fmgen_opna.cpp:1413-1443) loads WAV files -# with uppercase extension (.WAV) but the built-in rhythm engine (rhythmc.c) -# uses lowercase (.wav). Both paths resolve from np2kai/. +# with uppercase names (2608_BD.WAV). The built-in rhythm engine (rhythmc.c) +# uses lowercase (2608_bd.wav). Both resolve from np2kai/. # The fmgen engine also accepts "2608_RYM.WAV" as fallback for the rim sample. +# +# Sound ROM has board-specific variants tried before the generic fallback: +# sound26.rom (26K), sound86.rom (86), sound118.rom (118), soundSPB.rom +# (Speak Board), soundMO.rom (MO), sound14.rom (14). Built as "sound" + +# board name + ".rom" in soundrom.c:21-33. files: # -- Main BIOS ROM -- - # Loaded in bios/bios.c:430-440. 96 KB (0x18000) mapped at 0xe8000. - # Without this, the built-in BIOS simulator is used (less compatible). + # Loaded in bios/bios.c:430-436 via getbiospath(). 96 KB (0x18000) mapped + # at 0xe8000. Only loaded when np2cfg.usebios is true (core option). + # Without this, CopyMemory copies nosyscode[] built-in simulator. - name: "bios.rom" path: "np2kai/bios.rom" + size: 98304 required: false hle_fallback: true note: > - PC-9801 system BIOS ROM (96 KB). Provides full hardware compatibility. - The core can boot without it using the built-in BIOS simulator, but - some software may not work correctly. Loaded at address 0xe8000. - source_ref: "bios/bios.c:430-440, common/strres.c:60" - - # -- ITF ROM -- - # Initial Test Firmware, loaded at ITF_ADRS (0xf8000), 32 KB. - # Only loaded when BIOS_SIMULATE is not defined (bios/bios.c:569-574). - # In the libretro build, BIOS_SIMULATE is typically enabled, so this is - # only needed for non-simulated builds. - - name: "itf.rom" - path: "np2kai/itf.rom" - required: false - hle_fallback: true - note: > - Initial Test Firmware ROM (32 KB). Used for hardware initialization - and memory check at boot. Only loaded when the built-in ITF simulator - is disabled. Most libretro builds include the simulator. - source_ref: "bios/bios.c:569-574" + PC-9801 system BIOS ROM (96 KB). The core boots without it using the + built-in BIOS simulator, but some software requires the real BIOS. + source_ref: "bios/bios.c:430-436, common/strres.c:60" # -- Font file -- - # Set explicitly in libretro.c:1813 as "/np2kai/font.bmp". - # The core also supports FONT.ROM / font.rom (V98 format) via font_load(). - # Without any font file, built-in 8x8 bitmap data is used but kanji - # characters will not display correctly. + # Path set in libretro.c:1813 as "/np2kai/font.bmp". + # font_load() in font/font.c:125 detects type by extension: + # .bmp/.BMP = PC98 format (fontpc98.c), FONT.ROM = V98 format (fontv98.c). + # Without any font file, fontdata_8 provides 8x8 ASCII only. - name: "font.bmp" path: "np2kai/font.bmp" required: false hle_fallback: true aliases: ["FONT.ROM", "font.rom", "FONT.BMP"] note: > - PC-98 font bitmap (288 KB). Required for correct Japanese kanji display. - Without this file, only basic ASCII characters render correctly using - built-in data. - source_ref: "sdl/libretro/libretro.c:1813, font/fontdata.c:11-14" + PC-98 font bitmap. Required for correct Japanese kanji display. + Without this, only basic ASCII renders using built-in 8x8 data. + FONT.ROM (V98 format) is also accepted. + source_ref: "sdl/libretro/libretro.c:1813, font/font.c:86-125, font/fontdata.c:11" # -- Sound BIOS ROM -- - # Loaded by soundrom_load() as "sound.rom" (soundrom.c:15-16, 28-32). - # The filename is composed as "sound" + optional board name + ".rom". - # 16 KB ROM for the FM sound board. + # Loaded by soundrom_load() in soundrom.c:93-106 via loadsoundrom(). + # Filename composed as "sound" + optional board name + ".rom" (soundrom.c:27-32). + # Board-specific variants tried first, then sound.rom as fallback. + # 16 KB (0x4000). Falls back to 9-byte defsoundrom[] stub. - name: "sound.rom" path: "np2kai/sound.rom" + size: 16384 required: false hle_fallback: true note: > - FM sound board BIOS ROM (16 KB). Used by the PC-9801-26K/86/118 - sound boards. The core falls back to a minimal built-in default - (9-byte stub) if this file is missing. + FM sound board BIOS ROM (16 KB). Generic fallback for all sound boards. + Board-specific variants are tried first: sound26.rom (PC-9801-26K), + sound86.rom (86), sound118.rom (118), soundSPB.rom (Speak Board), + soundMO.rom (MO), sound14.rom (14). source_ref: "sound/soundrom.c:15-16,21-55,93-106" # -- YM2608 OPNA rhythm samples -- - # Loaded by both the built-in rhythm engine (rhythmc.c:60-71) and the - # fmgen engine (fmgen_opna.cpp:1413-1443). Required for YM2608 OPNA - # rhythm sound channel (bass drum, snare, etc). - # The fmgen engine tries uppercase .WAV, the built-in engine uses .wav. - # Place lowercase versions - the filesystem handles case on most platforms. + # Loaded by the built-in rhythm engine (rhythmc.c:60-71) using lowercase + # filenames, and by the fmgen engine (fmgen_opna.cpp:1413-1443) using + # uppercase (2608_BD.WAV etc). Both resolve via getbiospath(). - name: "2608_bd.wav" path: "np2kai/2608_bd.wav" required: false + aliases: ["2608_BD.WAV"] note: "YM2608 OPNA rhythm sample: bass drum" - source_ref: "sound/rhythmc.c:11, sound/fmgen/fmgen_opna.cpp:1431-1433" + source_ref: "sound/rhythmc.c:11,60-71, sound/fmgen/fmgen_opna.cpp:1431" - name: "2608_sd.wav" path: "np2kai/2608_sd.wav" required: false + aliases: ["2608_SD.WAV"] note: "YM2608 OPNA rhythm sample: snare drum" - source_ref: "sound/rhythmc.c:12" + source_ref: "sound/rhythmc.c:12, sound/fmgen/fmgen_opna.cpp:1432" - name: "2608_top.wav" path: "np2kai/2608_top.wav" required: false + aliases: ["2608_TOP.WAV"] note: "YM2608 OPNA rhythm sample: top cymbal" - source_ref: "sound/rhythmc.c:13" + source_ref: "sound/rhythmc.c:13, sound/fmgen/fmgen_opna.cpp:1432" - name: "2608_hh.wav" path: "np2kai/2608_hh.wav" required: false + aliases: ["2608_HH.WAV"] note: "YM2608 OPNA rhythm sample: hi-hat" - source_ref: "sound/rhythmc.c:14" + source_ref: "sound/rhythmc.c:14, sound/fmgen/fmgen_opna.cpp:1432" - name: "2608_tom.wav" path: "np2kai/2608_tom.wav" required: false + aliases: ["2608_TOM.WAV"] note: "YM2608 OPNA rhythm sample: tom" - source_ref: "sound/rhythmc.c:15" + source_ref: "sound/rhythmc.c:15, sound/fmgen/fmgen_opna.cpp:1432" - name: "2608_rim.wav" path: "np2kai/2608_rim.wav" required: false - aliases: ["2608_RYM.WAV"] - note: "YM2608 OPNA rhythm sample: rim shot. fmgen also accepts 2608_RYM.WAV" - source_ref: "sound/rhythmc.c:16, sound/fmgen/fmgen_opna.cpp:1413-1443" + aliases: ["2608_RIM.WAV", "2608_RYM.WAV"] + note: "YM2608 OPNA rhythm sample: rim shot. fmgen also accepts 2608_RYM.WAV." + source_ref: "sound/rhythmc.c:16, sound/fmgen/fmgen_opna.cpp:1431-1441" # -- IDE BIOS ROM -- # Loaded by ideio.c:1913-1931. Tried in order: ide.rom, d8000.rom, - # bank3.bin, bios9821.rom. Only loaded when IDE BIOS is enabled in - # core options (np2cfg.idebios) and a real BIOS ROM is also present. + # bank3.bin, bios9821.rom. Only loaded when np2cfg.idebios is enabled + # and a real main BIOS ROM is present. Falls back to simulated IDE BIOS. - name: "ide.rom" path: "np2kai/ide.rom" + size: 8192 required: false hle_fallback: true aliases: ["d8000.rom", "bank3.bin", "bios9821.rom"] note: > - IDE controller BIOS ROM (8 KB). Required for real IDE BIOS emulation - (HDD boot from IDE). Without this, a simulated IDE BIOS is used. - source_ref: "cbus/ideio.c:1913-1931" + IDE controller BIOS ROM (8 KB). For IDE HDD boot support. + Without this, a simulated IDE BIOS is used. + source_ref: "cbus/ideio.c:1913-1941" # -- SCSI BIOS ROM -- - # Loaded by scsiio.c:219-231. Falls back to built-in scsibios[] stub. + # Loaded by scsiio.c:219. 16 KB (0x4000). Falls back to scsibios[] stub. - name: "scsi.rom" path: "np2kai/scsi.rom" + size: 16384 required: false hle_fallback: true note: > - SCSI controller BIOS ROM (16 KB). For PC-98 SCSI HDD support. - The core includes a built-in SCSI BIOS stub as fallback. - source_ref: "cbus/scsiio.c:219-231" + SCSI controller BIOS ROM (16 KB). The core includes a built-in + SCSI BIOS stub as fallback. + source_ref: "cbus/scsiio.c:214-231" + + # -- SASI BIOS ROM -- + # Loaded by sasiio.c:455. 4 KB (0x1000). Falls back to sasibios[] stub. + - name: "sasi.rom" + path: "np2kai/sasi.rom" + size: 4096 + required: false + hle_fallback: true + note: > + SASI controller BIOS ROM (4 KB). The core includes a built-in + SASI BIOS stub as fallback. + source_ref: "cbus/sasiio.c:451-467" # -- PCI BIOS ROM -- - # Loaded by pcidev.c:364-382. Tries pci.rom then bank0.bin. - # Falls back to built-in PCI BIOS simulation. + # Loaded by pcidev.c:360-382. Tries pci.rom then bank0.bin. + # 32 KB (0x8000). Falls back to built-in PCI BIOS simulation. - name: "pci.rom" path: "np2kai/pci.rom" + size: 32768 required: false hle_fallback: true aliases: ["bank0.bin"] note: > PCI BIOS ROM (32 KB). For PC-9821 PCI bus emulation. Without this, the built-in PCI BIOS simulator is used. - source_ref: "io/pcidev.c:360-382" + source_ref: "io/pcidev.c:356-382" # -- GPIB BIOS ROM -- - # Loaded by gpibio.c:327-356. + # Loaded by gpibio.c:327-356. 8 KB (0x2000). No built-in fallback: + # if missing, GPIB is disabled entirely (gpib.enable = 0). - name: "gpib.rom" path: "np2kai/gpib.rom" + size: 8192 required: false note: > - GP-IB interface BIOS ROM. Rarely needed, only for GP-IB peripheral - emulation. - source_ref: "cbus/gpibio.c:327-356" + GP-IB interface BIOS ROM (8 KB). If missing, GP-IB emulation + is disabled entirely. + source_ref: "cbus/gpibio.c:320-356" + + # -- Keyboard remapping config -- + # Loaded by keystat_initialize() in keystat.c:43 via getbiospath(). + # Plain text file mapping keyboard scancodes. If missing, default + # built-in table is used. + - name: "key.txt" + path: "np2kai/key.txt" + required: false + note: > + Keyboard remapping configuration (text file). User-created file + for custom keyboard layout. The core uses built-in defaults if absent. + source_ref: "keystat.c:43,113-148" diff --git a/emulators/numero.yml b/emulators/numero.yml index 66839b18..9db10529 100644 --- a/emulators/numero.yml +++ b/emulators/numero.yml @@ -1,7 +1,9 @@ emulator: Numero type: libretro +core_classification: community_fork source: "https://github.com/nbarkhina/numero" -profiled_date: "2026-03-18" +upstream: "https://github.com/sputt/wabbitemu" +profiled_date: "2026-03-24" core_version: "v1.0" display_name: "Texas Instruments TI-83 (Numero)" cores: @@ -10,38 +12,35 @@ systems: - ti-83 notes: | - Numero is a TI-83 family calculator emulator for libretro, based on the - Wabbitemu emulator. Supports TI-83, TI-83 Plus and TI-83 Silver Edition. + Libretro port of Wabbitemu, a TI-83 family calculator emulator. + Supports TI-83, TI-83 Plus and TI-83 Plus Silver Edition. - A calculator ROM dump is required to run. The core searches for ROM files - in the system directory root with a fixed priority order - (libretronew.cpp:575-594): + A calculator ROM dump is required. The core searches sequentially in the + system directory (libretronew.cpp:575-594): ti83se.rom first, then + ti83plus.rom, then ti83.rom. The first found is loaded via rom_load() + (libretronew.cpp:608). file_present_in_system() checks existence only + via VFS open/close (libretronew.cpp:451-472). No hash or size validation. - 1. ti83se.rom (TI-83 Silver Edition, recommended -- largest storage) - 2. ti83plus.rom (TI-83 Plus) - 3. ti83.rom (TI-83) + Without any ROM, the core shows a blank screen listing the expected + filenames (libretronew.cpp:1030-1042). No HLE fallback. - The first file found wins. file_present_in_system() checks existence only - (libretronew.cpp:451-472), then rom_load() loads it (libretronew.cpp:608). + Supports no-content launch (libretronew.cpp:556). Content files + (.8xp/.8xk/.8xg) are loaded on top of the running ROM via link cable + simulation. Auto-saves calculator state every 10 seconds + (libretronew.cpp:1050-1063). - There is no fallback or built-in ROM. If none of the three files are found, - the core starts without a BIOS and shows a blank screen with a status - message listing the expected filenames (libretronew.cpp:1034-1041). - - The core supports no-content launch (RETRO_ENVIRONMENT_SET_SUPPORT_NO_GAME - is set to true, libretronew.cpp:555-556). Content files are .8xp/.8xk/.8xg - calculator programs loaded on top of the running ROM. - - The emulator auto-saves calculator state every 10 seconds to preserve RAM - contents (the TI-83 has no persistent storage beyond battery-backed RAM). + Upstream Wabbitemu supports a wider model range (TI-73 through TI-84+CSE) + but the libretro wrapper only searches for TI-83 family filenames. The + underlying engine (calc.cpp:rom_load) auto-detects the model from the ROM + header. files: - name: "ti83se.rom" system: ti-83 - description: "TI-83 Silver Edition ROM dump" + description: "TI-83 Plus Silver Edition ROM dump" required: false source_ref: "libretronew.cpp:576-578" - notes: "Checked first. Recommended by the author for largest storage capacity." + notes: "Checked first. Recommended for largest storage capacity." - name: "ti83plus.rom" system: ti-83 @@ -55,28 +54,4 @@ files: description: "TI-83 ROM dump" required: false source_ref: "libretronew.cpp:591-593" - notes: "Checked last. Original TI-83 (non-Plus) ROM." - -platform_details: - bios_search: - source_ref: "libretronew.cpp:451-472, 575-608" - notes: | - file_present_in_system() opens a read handle via libretro VFS to - test file existence. The search is sequential: ti83se.rom first, - then ti83plus.rom, then ti83.rom. Only one ROM is loaded. - - rom_loading: - source_ref: "src/Interface/calc.cpp (rom_load)" - notes: | - rom_load() detects the calculator model from the ROM image header - and initializes the appropriate hardware (83, 83+, 83SE). Each - model gets its own progress/save directory via setProgressDir(). - - content_loading: - formats: "8xp, 8xk, 8xg" - source_ref: "libretronew.cpp:483, 769" - notes: | - Calculator programs (.8xp), apps (.8xk) and groups (.8xg) are - loaded into the running calculator via the sendfile mechanism, - simulating the link cable transfer. Multiple programs can be - loaded sequentially onto the same ROM session. + notes: "Checked last. Original TI-83 (non-Plus)." diff --git a/emulators/nxengine.yml b/emulators/nxengine.yml index b5933cb4..5d753b55 100644 --- a/emulators/nxengine.yml +++ b/emulators/nxengine.yml @@ -1,45 +1,38 @@ emulator: NXEngine type: libretro +core_classification: game_engine source: "https://github.com/libretro/nxengine-libretro" -profiled_date: "2026-03-18" +upstream: "https://github.com/Rox64/NXEngine" +profiled_date: "2026-03-24" core_version: "1.0.0.6" display_name: "Cave Story (NXEngine)" +cores: [nxengine] systems: [cave-story] notes: | NXEngine is an open-source reimplementation of the Cave Story (Doukutsu - Monogatari) engine by Studio Pixel. It is not an emulator but a source port - that loads the original freeware game data directly. + Monogatari) engine by Studio Pixel, authored by Caitlin Shaw. It is not + an emulator but a source port that loads the original freeware game data. - The core requires the original freeware Cave Story distribution placed in - {system_dir}/nxengine/. When launched without a content file, it looks for - Doukutsu.exe at that path (libretro.cpp:254-259). When launched with a .exe - content file, it uses the parent directory of that file instead - (libretro.cpp:244). + The core requires the freeware Cave Story distribution placed in + {system_dir}/nxengine/. When launched without content, it looks for + Doukutsu.exe there (libretro.cpp:254-259). When launched with a .exe + content file, it uses the parent directory of that file (libretro.cpp:237). - At startup (main.cpp:66-90), the core opens Doukutsu.exe and extracts from - it: ORG music files (extractorg.c), PXT sound effects (extractpxt.c), stage - tile attributes (extractstages.c), bitmap graphics, and the wavetable - (cachefiles.c:484). These are kept in memory, not written to disk. + At startup (main.cpp:75-90), the core opens Doukutsu.exe and extracts: + ORG music (extractorg.c), PXT sound effects (extractpxt.c), stage tile + attributes (extractstages.c), 18 credit BMPs + pixel.bmp (endpic/), + and the wavetable (cachefiles.c:464-485). All kept in memory. - The data/ directory must exist alongside Doukutsu.exe with the full game - asset tree: 30 root-level files (sprites, backgrounds, script tables), - 36 NPC sprite sheets in data/Npc/, and 333 stage files in data/Stage/ - (maps .pxm, tile attributes .pxa, entity lists .pxe, scripts .tsc, and - tileset images via Prt*.pbm). The core verifies data/ exists by checking - for data/npc.tbl (main.cpp:47-63). + The data/ directory (399 files from the freeware release) is loaded into + an in-memory cache at init (cachefiles.c:38-439, 497-540). All subsequent + file access goes through this cache. The core verifies data/ by checking + for data/npc.tbl (main.cpp:47-58). - The core also reads endpic/ bitmaps for ending sequences, extracted from - Doukutsu.exe at runtime (cachefiles.c:460-484). + The font is compiled-in (bitmap_font.h). The upstream standalone version + uses font.ttf via SDL_ttf. - All required files ship in the datafiles/ directory of the libretro repo - itself, since Cave Story is freeware. No separate BIOS or firmware is - needed. The entire freeware distribution is the "system" requirement. - - sprites.sif has a compiled-in fallback (sprites_sif.h) and is the only - data file that can be missing without a fatal error (cachefiles.c:515-521). - - Valid content extension: .exe (retro_get_system_info, libretro.cpp:96). + Valid content extension: .exe (libretro.cpp:109). files: - name: "Doukutsu.exe" @@ -51,20 +44,37 @@ files: md5: "38695d3d69d7a0ada8178072dad4c58b" sha1: "bb2d0441e073da9c584f23c2ad8c7ab8aac293bf" source_ref: "main.cpp:77-78 (opened for extraction), libretro.cpp:258 (existence check)" - notes: "Placed in system/nxengine/. The core extracts ORG music, PXT sounds, stage tile attributes, BMP graphics, and wavetable.dat from this binary at each launch." - - - name: "data/npc.tbl" - category: game_data - system: cave-story - description: "NPC attribute table (entity behavior flags, HP, damage, display rect offsets)" - required: true - source_ref: "main.cpp:50 (existence check for data/ directory validation)" - notes: "Located in system/nxengine/data/. The core uses this file to verify the data directory is present." + notes: "Placed in system/nxengine/. The core extracts ORG music, PXT sounds, stage tile attributes, BMP graphics, and wavetable from this binary at each launch." - name: "data/" category: game_data system: cave-story description: "Full game asset directory tree (399 files: sprites, NPC sheets, stage maps, scripts, backgrounds)" required: true - source_ref: "cachefiles.c:38-480 (complete file list loaded at init)" + source_ref: "cachefiles.c:38-439 (filenames[] array loaded at init)" notes: "Must contain root assets (Arms.pbm, MyChar.pbm, etc.), Npc/ (36 sprite sheets), and Stage/ (333 map/script/tileset files). All files from the original freeware release." + + - name: "data/npc.tbl" + category: game_data + system: cave-story + description: "NPC attribute table (entity behavior flags, HP, damage, display rect offsets)" + required: true + source_ref: "main.cpp:50 (existence check for data/ directory), ai/ai.cpp:56-59 (loaded for NPC properties)" + notes: "Located in system/nxengine/data/. Used to validate data directory presence and to load NPC behavior attributes." + + - name: "data/sprites.sif" + category: game_data + system: cave-story + description: "Sprite information file (sprite positions, sizes, animation data)" + required: false + hle_fallback: true + source_ref: "cachefiles.c:101 (in filenames[]), cachefiles.c:515-521 (compiled-in fallback from sprites_sif.h)" + notes: "Not shipped in the freeware distribution. If missing, the core uses a compiled-in copy (sprites_sif.h)." + + - name: "tilekey.dat" + category: game_data + system: cave-story + description: "Tile attribute lookup table (maps tile codes to collision/behavior attributes)" + required: false + source_ref: "map.cpp:290-303 (loaded at init, hardcoded default if missing)" + notes: "Not part of the freeware distribution. Generated by the standalone NXEngine extraction tool. The libretro core has hardcoded defaults in map.cpp:30." diff --git a/emulators/o2em.yml b/emulators/o2em.yml index e7756c9a..565a4cb6 100644 --- a/emulators/o2em.yml +++ b/emulators/o2em.yml @@ -1,8 +1,11 @@ emulator: O2EM type: libretro +core_classification: community_fork core: o2em_libretro +cores: [o2em_libretro] source: "https://github.com/libretro/libretro-o2em" -profiled_date: "2026-03-18" +upstream: "https://sourceforge.net/projects/o2em/" +profiled_date: "2026-03-24" core_version: "1.18" display_name: "Magnavox - Odyssey2 / Philips Videopac+ (O2EM)" systems: @@ -10,28 +13,24 @@ systems: - videopac notes: | - O2EM is the libretro port of the Odyssey2/Videopac emulator. - The core requires exactly one BIOS ROM selected via the o2em_bios core option. - Default is o2rom.bin (Odyssey2 NTSC). The user can switch to c52.bin, g7400.bin - or jopac.bin for European/French Videopac variants. + Libretro port of O2EM, an Odyssey2/Videopac emulator by Daniel Boris + and André de la Rocha. Port by Arlindo M. de Oliveira. - BIOS files are loaded from the system directory into rom_table[0] (1024 bytes). - The core identifies the BIOS variant by CRC32 and sets the vpp flag accordingly - (vpp=1 enables Videopac+ enhanced graphics for g7400.bin and jopac.bin). + One BIOS ROM selected via o2em_bios core option (default o2rom.bin). + Loaded into rom_table[0] (1024 bytes), identified by CRC32. + vpp=1 for Videopac+ variants (g7400.bin, jopac.bin). + Core fails to load without the selected BIOS. - The core will not start without a valid BIOS file present. - Any single BIOS from the list below is sufficient for its region/hardware. + Voice module ("The Voice" speech synthesis add-on) emulation via WAV + samples in system/voice/. 9 banks × 128 samples, named {bank}{sample}.WAV + (e.g. E480.WAV). Compiled with HAVE_VOICE=1 on most platforms. Optional: + core works without samples, speech is silently skipped. - All BIOS files are exactly 1 KB (1024 bytes). - - BIOS detection: libretro.c load_bios() lines 146-212, CRC32 switch. - BIOS selection: libretro_core_options.h o2em_bios option lines 52-64. - Core option handling: libretro.c lines 1107-1134. + BIOS loading: libretro.c load_bios() lines 145-212. + BIOS selection: libretro_core_options.h lines 52-64. + Voice loading: voice.c init_voice() lines 38-79, called from libretro.c:1025-1032. files: - # ------------------------------------------------------- - # Magnavox Odyssey2 (NTSC) - G7000 model - # ------------------------------------------------------- - name: "o2rom.bin" system: odyssey2 region: [north-america] @@ -41,12 +40,9 @@ files: sha1: "b2e1955d957a475de2411770452eff4ea19f4cee" crc32: "8016a315" validation: [size, crc32] - note: "Magnavox Odyssey2 BIOS (G7000 NTSC). Default BIOS, vpp=0." + note: "Odyssey2 BIOS (G7000 NTSC). Default, vpp=0." source_ref: "libretro.c:182-186" - # ------------------------------------------------------- - # Philips Videopac G7000 (European) - # ------------------------------------------------------- - name: "c52.bin" system: videopac region: [europe] @@ -56,12 +52,9 @@ files: sha1: "a6120aed50831c9c0d95dbdf707820f601d9452e" crc32: "a318e8d6" validation: [size, crc32] - note: "Philips Videopac G7000 European BIOS. vpp=0, auto-sets PAL region." + note: "Videopac G7000 European BIOS. vpp=0, auto-sets PAL." source_ref: "libretro.c:192-197" - # ------------------------------------------------------- - # Philips Videopac+ G7400 (European) - # ------------------------------------------------------- - name: "g7400.bin" system: videopac region: [europe] @@ -71,12 +64,9 @@ files: sha1: "5130243429b40b01a14e1304d0394b8459a6fbae" crc32: "e20a9f41" validation: [size, crc32] - note: "Philips Videopac+ G7400 European BIOS. vpp=1, enables enhanced graphics." + note: "Videopac+ G7400 European BIOS. vpp=1." source_ref: "libretro.c:187-191" - # ------------------------------------------------------- - # Philips Videopac+ G7400 (French) - JoPac - # ------------------------------------------------------- - name: "jopac.bin" system: videopac region: [france] @@ -86,20 +76,11 @@ files: sha1: "54b8d2c1317628de51a85fc1c424423a986775e4" crc32: "11647ca5" validation: [size, crc32] - note: "Philips Videopac+ G7400 French BIOS (JoPac). vpp=1, enables enhanced graphics." + note: "Videopac+ G7400 French BIOS (JoPac). vpp=1." source_ref: "libretro.c:198-203" -platform_details: - odyssey2: - bios_size: 1024 - bios_selection: "core option o2em_bios, default o2rom.bin" - detection_method: "CRC32 of 1024-byte ROM" - hle_available: false - source_ref: "libretro.c:146-212, libretro_core_options.h:52-64" - videopac: - bios_size: 1024 - bios_selection: "core option o2em_bios, choose c52/g7400/jopac" - detection_method: "CRC32 of 1024-byte ROM" - vpp_flag: "g7400.bin and jopac.bin set vpp=1 for enhanced graphics" - hle_available: false - source_ref: "libretro.c:146-212, libretro_core_options.h:52-64" + - name: "voice/" + required: false + category: game_data + note: "The Voice speech synthesis WAV samples. 9 banks (E4, E8-EF) × 128 samples." + source_ref: "voice.c:38-79, libretro.c:1025-1032" diff --git a/emulators/oberon.yml b/emulators/oberon.yml index 672cfdd1..6ad771fb 100644 --- a/emulators/oberon.yml +++ b/emulators/oberon.yml @@ -1,9 +1,10 @@ emulator: Oberon type: libretro +core_classification: community_fork source: "https://github.com/libretro/oberon-risc-emu" -profiled_date: "2026-03-18" +upstream: "https://github.com/pdewacht/oberon-risc-emu" +profiled_date: "2026-03-24" core_version: "2020-07-01" -display_name: "Project Oberon RISC" display_name: "Oberon RISC Emulator" cores: - oberon @@ -11,33 +12,15 @@ systems: [oberon] # Project Oberon RISC emulator by Peter De Wachter. # Emulates the Oberon RISC processor designed by Niklaus Wirth. -# The bootloader (512 words) is compiled into the binary from risc-boot.inc, -# loaded into ROM at 0xFFFFF800 on startup (risc.c:75-77, risc_new). -# -# Content: .dsk disk images containing the full Oberon operating system. -# The disk image is loaded via retro_load_game(game->path) and attached -# as SPI disk (Libretro/libretro.c:209-214). No files are read from -# the RetroArch system directory. -# -# Reference disk images ship in the repo under DiskImage/: -# Oberon-2020-08-18.dsk (990208 bytes, latest) -# Oberon-2019-01-21.dsk (988160 bytes) -# Oberon-2016-08-02.dsk (989184 bytes) -# Oberon-2016-04-18.dsk (989184 bytes) -# These are game content, not system files. +# Bootloader (512 words) compiled into the binary from risc-boot.inc, +# loaded into ROM at 0xFFFFF800 on startup (risc.c:75-77,93). +# No retro_get_system_directory() call — no files from system dir. +# Content: .dsk disk images loaded via retro_load_game (libretro.c:209-214). +# Upstream and libretro core emulation code (risc.c, disk.c) are identical. files: [] notes: - architecture: > - Custom 32-bit RISC CPU (25 MHz emulated) with 1 MB RAM (expandable to 32 MB). - Monochrome 1-bit framebuffer. Keyboard input via PS/2 scancodes. - SPI bus for SD card (disk image) access. Serial port for PCLink file transfer. boot_process: > - CPU starts execution at ROM address 0xFFFFF800. The embedded bootloader - reads the boot sector from the SPI disk and loads the Oberon inner core - (modules Kernel, FileDir, Files, Modules) into RAM, then jumps to it. - content_format: > - Disk images (.dsk) are raw sector images read via 512-byte SPI commands. - The core detects filesystem-only images (magic 0x9B1EA38D at sector 0) - and adjusts the sector offset accordingly (disk.c:57-58). + Bootloader embedded in ROM reads boot sector from SPI disk, loads + Oberon inner core into RAM. Content is raw sector .dsk images. diff --git a/emulators/onscripter.yml b/emulators/onscripter.yml index 19dc3ffe..e5eedf25 100644 --- a/emulators/onscripter.yml +++ b/emulators/onscripter.yml @@ -1,7 +1,9 @@ emulator: ONScripter type: libretro +core_classification: community_fork source: "https://github.com/iyzsong/onscripter-libretro" -profiled_date: "2026-03-18" +upstream: "https://github.com/ogapee/onscripter" +profiled_date: "2026-03-24" core_version: "0.3" display_name: "ONScripter" cores: @@ -10,30 +12,18 @@ systems: - onscripter notes: | - ONScripter is a clone of NScripter, a Japanese visual novel engine by - Naoki Takahashi. The libretro port by iyzsong wraps the upstream engine - (ogapee/onscripter, tag 20230825) with an SDL-to-libretro shim layer. + Clone of NScripter, a Japanese visual novel engine. The libretro port + wraps upstream ogapee/onscripter (tag 20230825) via an SDL-to-libretro + shim. - No BIOS or system files are required. The .info file declares no - firmware entries and the core never calls - RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY. + No system files required. The .info declares no firmware. The core + never calls RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY. All files + (scripts, fonts, archives, config) are loaded from the game directory + via archive_path (libretro.cpp:120-121, ScriptHandler.cpp:148-161). - Game content is loaded directly from the archive path containing the - script file. The core opens txt, dat, ___, or ons script files via - ONScripter::openScript() and reads all game assets (graphics, audio, - fonts) from the same directory. + default.ttf, registry.txt, dll.txt are per-game assets shipped with + each visual novel, not system-level files. - The engine looks for a font file named default.ttf in the game - directory. This is a per-game asset shipped with each visual novel, - not a system-level file managed by RetroArch. - - Dependencies (bundled at build time via Guix): freetype, libjpeg, - libogg, libvorbis, libmad, bzip2, SDL (shimmed to libretro). - - The core is marked is_experimental = true. It does not support - save states (retro_serialize_size returns 0). - - Supported script formats: .txt, .dat, .___, .ons - needs_fullpath = true, block_extract = false. + Experimental core. No save state support (retro_serialize_size = 0). files: [] diff --git a/emulators/onsyuri.yml b/emulators/onsyuri.yml index 7bf8b289..746954aa 100644 --- a/emulators/onsyuri.yml +++ b/emulators/onsyuri.yml @@ -1,7 +1,9 @@ emulator: ONScripter Yuri type: game -source: "https://github.com/libretro/libretro-onsyuri" -profiled_date: "2026-03-18" +core_classification: game_engine +source: "https://github.com/YuriSizuku/OnscripterYuri" +upstream: "https://github.com/YuriSizuku/OnscripterYuri" +profiled_date: "2026-03-24" core_version: "0.7.4+2" display_name: "ONScripter Yuri" cores: @@ -10,13 +12,10 @@ systems: - onscripter notes: | - ONScripter Yuri is a fork of ONScripter optimized for the libretro - environment. Like the original ONScripter, it is a clone of the - NScripter visual novel engine and runs .txt, .dat, and .ons script - files. - - No BIOS or system files required. Game content (scripts, graphics, - audio, fonts) is loaded from the game directory. Each game ships its - own default.ttf font file. + Clone of the NScripter visual novel engine, forked from ONScripter-Jh. + The libretro port lives in the upstream repo (src/onsyuri_libretro/). + No system files required. Game scripts (0.txt, nscript.dat, etc.), + fonts (default.ttf), and assets are loaded from the game directory. + The core does not use RetroArch's system directory. files: [] diff --git a/emulators/openlara.yml b/emulators/openlara.yml index f5fcc353..3ae8896a 100644 --- a/emulators/openlara.yml +++ b/emulators/openlara.yml @@ -1,7 +1,9 @@ emulator: OpenLara type: libretro +core_classification: game_engine source: "https://github.com/libretro/OpenLara" -profiled_date: "2026-03-18" +upstream: "https://github.com/XProger/OpenLara" +profiled_date: "2026-03-24" core_version: "v1" display_name: "Tomb Raider (OpenLara)" cores: [openlara] @@ -9,23 +11,14 @@ systems: [tomb-raider] verification: existence notes: > Open-source reimplementation of the classic Tomb Raider engine (TR1-TR4). - Supports PC (.PHD, .TR2, .TR4), PlayStation (.PSX), and Saturn (.SAT) - level formats. The core accepts phd|psx|tr2|sat extensions - (retro_get_system_info sets valid_extensions, main.cpp:246). - need_fullpath is true - the core resolves sibling game assets relative - to the content directory. retro_load_game (main.cpp:602) derives - contentDir from the loaded level file path and strips the level/ - subdirectory if present, then passes a relative levelpath to the engine. - Game version is auto-detected by probing for known level files: - DATA/GYM.PHD (TR1 PC), PSXDATA/GYM.PSX (TR1 PSX), DATA/GYM.SAT - (TR1 Saturn), data/ASSAULT.TR2 (TR2 PC), DATA/ASSAULT.PSX (TR2 PSX), - data/JUNGLE.TR2 (TR3 PC), DATA/JUNGLE.PSX (TR3 PSX), data/angkor1.tr4 - (TR4 PC) - see gameflow.h:1047-1071. Audio tracks are loaded from - audio/ subdirectories as .ogg files or cdaudio.wad containers - (gameflow.h:1248-1350). The system directory is used only to create - a cache folder at {system_dir}/openlara/cache/ (main.cpp:177-197). - No BIOS, firmware, or engine data files are required in the system - directory. All required data comes from the original game files loaded - as content. + Accepts phd|psx|tr2|sat extensions (main.cpp:246). need_fullpath is true - + the core resolves game assets relative to the content directory. + retro_load_game (main.cpp:602) derives contentDir from the level file path + and strips level/ if present. Game version auto-detected by probing for + known level files (gameflow.h:1047-1071). Audio tracks loaded from audio/ + subdirectories as .ogg/.mp3/.wav files or cdaudio.wad (gameflow.h:1272-1356). + System directory used only for shader cache at {system_dir}/openlara/cache/ + (main.cpp:177-197). No BIOS, firmware, or engine data files required in + system directory. .info omits sat from supported_extensions (code declares it). files: [] diff --git a/emulators/opentyrian.yml b/emulators/opentyrian.yml index 4e37ec14..a17cfa81 100644 --- a/emulators/opentyrian.yml +++ b/emulators/opentyrian.yml @@ -1,7 +1,9 @@ emulator: OpenTyrian type: libretro +core_classification: game_engine source: "https://github.com/trapexit/libretro-opentyrian" -profiled_date: "2026-03-18" +upstream: "https://github.com/opentyrian/opentyrian" +profiled_date: "2026-03-24" core_version: "1.0.0.6" display_name: "Tyrian (OpenTyrian)" cores: [opentyrian] diff --git a/emulators/opera.yml b/emulators/opera.yml index c8dd52f8..536b94a4 100644 --- a/emulators/opera.yml +++ b/emulators/opera.yml @@ -1,8 +1,11 @@ emulator: Opera (4DO) type: libretro +core_classification: community_fork core: opera_libretro +cores: [opera] source: "https://github.com/libretro/opera-libretro" -profiled_date: "2026-03-18" +upstream: "https://sourceforge.net/projects/freedo/" +profiled_date: "2026-03-24" core_version: "1.0.0" display_name: "The 3DO Company - 3DO (Opera)" systems: @@ -27,7 +30,8 @@ notes: | BIOS definitions: libopera/opera_bios.c BIOSES[] lines 3-136. BIOS loading: opera_lr_opts.c opera_lr_opts_set_bios() lines 239-270. - Font loading: opera_lr_opts.c opera_lr_opts_set_font() lines 272-320. + Font loading: opera_lr_opts.c opera_lr_opts_get_font() lines 274-293, + opera_lr_opts_set_font() lines 297-328. Core option: libretro_core_options.c opera_bios / opera_font. files: diff --git a/emulators/panda3ds.yml b/emulators/panda3ds.yml index 9559ca36..2c7dc652 100644 --- a/emulators/panda3ds.yml +++ b/emulators/panda3ds.yml @@ -1,36 +1,36 @@ emulator: Panda3DS -type: libretro +type: standalone + libretro +core_classification: official_port core_name: panda3ds_libretro source: "https://github.com/panda3ds-emu/panda3ds" -profiled_date: "2026-03-18" +upstream: "https://github.com/panda3ds-emu/panda3ds" +profiled_date: "2026-03-24" core_version: "Git" display_name: "Nintendo - 3DS (Panda3DS)" +cores: [panda3ds] systems: [nintendo-3ds] notes: | - Panda3DS is an HLE 3DS emulator. Most games run without any system files. + HLE 3DS emulator. Most games run without system files. Encrypted ROMs need AES keys in sysdata/aes_keys.txt, and seed-encrypted titles (9.6+) also need sysdata/seeddb.bin. - DSP firmware is loaded from the game itself (not from disk), with HLE/LLE/Null - modes selectable via core option panda3ds_dsp_emulation. - System archives (shared font, bad word list, country list, mii data) are - compiled into the binary from citra_system_archives headers. - The libretro core does NOT use RetroArch's system directory. It stores data - under the save directory in "Emulator Files/sysdata/". - The .info file declares no firmware entries, so RetroArch will not check - for any system files. - Experimental core: is_experimental = true in the .info file. + DSP firmware is loaded from the game itself (HLE/LLE/Null modes via core + option panda3ds_dsp_emulation). System archives (shared font, bad word + list, country list, mii data) are compiled into the binary via cmrc. + The libretro core uses the save directory, not RetroArch's system + directory. Files go under /Emulator Files/sysdata/. + The .info declares firmware_count=0. is_experimental = true. files: - name: "aes_keys.txt" path: "Emulator Files/sysdata/aes_keys.txt" description: "AES encryption keys for decrypting encrypted ROMs" required: false - source_ref: "src/emulator.cpp:229,238" + source_ref: "src/emulator.cpp:229,237-238, src/core/crypto/aes_engine.cpp:13-92" notes: | Loaded at ROM load time from appDataRoot/sysdata/aes_keys.txt. In libretro mode, appDataRoot = /Emulator Files/. - Contains key slot entries like "generator=XXXX", keyX/keyY values. + Contains key slot entries (generator, keyX, keyY, normalKey). Only needed for encrypted .3ds/.cci/.cxi/.app files. Decrypted dumps work without this file. @@ -38,8 +38,8 @@ files: path: "Emulator Files/sysdata/seeddb.bin" description: "Seed database for seed-encrypted games" required: false - source_ref: "src/emulator.cpp:230,241-242, src/core/loader/ncch.cpp:77-93" + source_ref: "src/emulator.cpp:230,241-242, src/core/loader/ncch.cpp:78-93" notes: | - Required for titles using seed encryption (firmware 9.6+). - Must be placed alongside aes_keys.txt in the sysdata directory. - Without it, seed-encrypted titles will fail to load with a warning. + Used for titles with seed encryption (firmware 9.6+). + Placed alongside aes_keys.txt in the sysdata directory. + Without it, seed-encrypted titles fail to load with a warning. diff --git a/emulators/parallel_n64.yml b/emulators/parallel_n64.yml index b2ae4088..1b441d5d 100644 --- a/emulators/parallel_n64.yml +++ b/emulators/parallel_n64.yml @@ -1,16 +1,33 @@ emulator: "ParaLLEl N64" type: libretro +core_classification: community_fork source: "https://github.com/libretro/parallel-n64" +upstream: "https://github.com/mupen64plus/mupen64plus-core" profiled_date: "2026-03-24" core_version: "2.0-rc2" display_name: "Nintendo - Nintendo 64 (ParaLLEl N64)" systems: [nintendo-64, nintendo-64dd] cores: [parallel_n64, parallel_n64_debug] -# Needs full source-verified profiling. Minimal profile based on known file paths. - files: - name: "64DD_IPL.bin" path: "64DD_IPL.bin" required: false - note: "64DD IPL ROM. Only needed for N64 Disk Drive games" + description: "64DD IPL ROM" + note: "Only loaded when a 64DD disk image (.ndd) is present via subsystem API. Core fails to load disk games without it." + source_ref: "libretro/libretro.c:576-623" + +notes: + hle_available: true + hle_note: > + PIF boot ROM is fully HLE'd (pifbootrom/pifbootrom.c:57). CIC + challenge/response handled in software (n64_cic_nus_6105.c). No BIOS + files needed for standard cartridge games. + dd_note: > + 64DD IPL ROM loaded from system_dir/64DD_IPL.bin when disk_data is + present. Loaded via fopen with no hash or size validation. + divergence_note: > + Fork of mupen64plus-core with significant divergence. Includes legacy + GPU plugins (Glide64, glN64, Rice, angrylion) and ParaLLEl-RDP/RSP. + Uses flat path 64DD_IPL.bin unlike mupen64plus-next which uses + Mupen64plus/IPL.n64 subdirectory. diff --git a/emulators/parallel_n64_debug.yml b/emulators/parallel_n64_debug.yml index 41bef225..e45e68e0 100644 --- a/emulators/parallel_n64_debug.yml +++ b/emulators/parallel_n64_debug.yml @@ -1,8 +1,8 @@ emulator: "parallel_n64_debug" type: alias -alias_of: "mupen64plus" -profiled_date: "2026-03-18" +alias_of: "parallel_n64" +profiled_date: "2026-03-24" core_version: "2.0-rc2" display_name: "Nintendo - Nintendo 64 (ParaLLEl) (Debug)" -note: "This core uses the same BIOS/firmware as mupen64plus. See emulators/mupen64plus.yml for details." +note: "Debug build of parallel_n64. Same codebase and firmware requirements." files: [] diff --git a/emulators/pascal_pong.yml b/emulators/pascal_pong.yml index 0f8f1fbb..fc3b16a8 100644 --- a/emulators/pascal_pong.yml +++ b/emulators/pascal_pong.yml @@ -1,13 +1,14 @@ emulator: Pascal Pong type: game -source: "https://github.com/libretro/libretro-pong" -profiled_date: "2026-03-18" +core_classification: pure_libretro +source: "https://github.com/libretro/pascal-pong-libretro" +upstream: "https://github.com/libretro/pascal-pong-libretro" +profiled_date: "2026-03-24" core_version: "v1.0" display_name: "PascalPong" cores: [pascal_pong] systems: [] files: [] notes: > - Pong clone written in Pascal as a libretro core demonstration. - Self-contained with all rendering done programmatically. - No content file, BIOS, or system directory files required. + Pong clone written in Pascal for libretro. Self-contained, + all assets embedded in the binary. No external files loaded. diff --git a/emulators/pcem.yml b/emulators/pcem.yml index dbf39b0e..d048dae8 100644 --- a/emulators/pcem.yml +++ b/emulators/pcem.yml @@ -1,17 +1,45 @@ emulator: PCem type: libretro +core_classification: community_fork source: "https://github.com/libretro/libretro-pcem" -profiled_date: "2026-03-18" -core_version: "SVN" +upstream: "https://github.com/sarah-walker-pcem/pcem" +profiled_date: "2026-03-24" +core_version: "v11 (PCem-mooch, based on PCem v10.1)" display_name: "PC (PCem)" -systems: [ibm-pc, ibm-xt, ibm-at, ibm-pcjr, ibm-ps1, tandy-1000] +cores: [pcem] +systems: + - ibm-pc + - ibm-xt + - ibm-at + - ibm-pcjr + - ibm-ps1 + - tandy-1000 -# PCem (PC Emulator) emulates IBM PC compatibles from 8088 through Pentium. -# All ROMs are loaded relative to the core's system directory via romfopen() -# which prepends pcempath (= RetroArch system dir) to the path. -# Each machine model requires its own BIOS ROM set under roms//. -# Video card ROMs are separate and only needed for the selected GPU. -# The font ROM (mda.rom) is always loaded at startup. +notes: + rom_structure: > + All ROM files are loaded relative to the RetroArch system directory via + romfopen(). Machine BIOS ROMs go in roms// subdirectories. + Video card ROMs go directly in roms/ (or roms// for some). + Which ROMs are needed depends on the selected machine model and video card. + minimum_requirement: > + At minimum, mda.rom (font) and one machine BIOS set are required. + The video card ROM for the selected GPU is also needed unless the machine + has built-in video (PCjr, Tandy, Amstrad, Acer 386SX, PS/1). + libretro_version: > + The libretro port is PCem-mooch v11, a community fork based on PCem v10.1. + Upstream PCem has evolved to v17 with many more machines and devices. + nvr_files: > + NVR files are CMOS/NVRAM templates providing pre-configured BIOS settings. + Shipped with the PCem distribution. Without them the BIOS starts with blank + CMOS (user must manually configure). The emulator also writes these back on + shutdown. + runtime_state: > + The emulator creates additional state files during operation: ATI EEPROM + (ati18800.nvr, ati28800.nvr, mach64.nvr), Intel flash (dmi.bin, escd.bin + per Pentium machine). These are emulator-generated and not included here. + info_divergence: > + The .info declares zero firmware (no firmware_count field). The core + actually loads 89+ ROM files plus NVR/EEPROM templates. files: # ======================================================== @@ -150,14 +178,18 @@ files: - name: "Tandy 1000 SL/2 BIOS low" path: "roms/tandy1000sl2/8079047.hu1" required: false - note: "Tandy 1000 SL/2 BIOS low chip. Paired with 8079048.hu2." - source_ref: "src/mem.c:180" + note: > + Tandy 1000 SL/2 BIOS low chip. Paired with 8079048.hu2. + Also loaded by tandy_rom.c for full bank-switched ROM (512 KB). + source_ref: "src/mem.c:180, src/tandy_rom.c:58" - name: "Tandy 1000 SL/2 BIOS high" path: "roms/tandy1000sl2/8079048.hu2" required: false - note: "Tandy 1000 SL/2 BIOS high chip. Paired with 8079047.hu1." - source_ref: "src/mem.c:181" + note: > + Tandy 1000 SL/2 BIOS high chip. Paired with 8079047.hu1. + Also loaded by tandy_rom.c for full bank-switched ROM (512 KB). + source_ref: "src/mem.c:181, src/tandy_rom.c:59" # ======================================================== # MACHINE BIOS ROMs - Amstrad / Sinclair @@ -360,7 +392,7 @@ files: - name: "Acer 386SX BIOS" path: "roms/acer386/acer386.bin" required: false - note: "Acer 386SX/25N BIOS (64 KB). Also requires oti067.bin video ROM." + note: "Acer 386SX/25N BIOS (64 KB)." source_ref: "src/mem.c:353-356" - name: "Acer 386SX OTI-067 video BIOS" @@ -634,8 +666,8 @@ files: path: "roms/mach64gx/bios.bin" required: false note: > - ATI Graphics Pro Turbo (Mach64 GX) video BIOS. Also used by the - Cirrus Logic CL-GD5429 init path (likely a copy/paste in the source). + ATI Graphics Pro Turbo (Mach64 GX) video BIOS. Also loaded by the + Cirrus Logic init path (copy/paste in source). source_ref: "src/vid_ati_mach64.c:2341, src/vid_cirrus.c:2324" - name: "Cirrus Logic CL-GD5429 BIOS" @@ -696,16 +728,224 @@ files: note: "NE2000 ISA Ethernet boot ROM (64 KB)." source_ref: "src/ne2000.c:1701" -notes: - rom_structure: > - All ROM files are loaded relative to the RetroArch system directory. - Machine BIOS ROMs go in roms// subdirectories. - Video card ROMs go directly in roms/ (or roms// for some). - Which ROMs are needed depends on the selected machine model and video card. - minimum_requirement: > - At minimum, the mda.rom font and one machine BIOS set are required. - The video card ROM for the selected GPU is also needed unless the machine - has built-in video (PCjr, Tandy, Amstrad, Acer 386SX, PS/1). - libretro_version: > - Based on PCem v10.1. The libretro port is quite old and may not match - current standalone PCem/86Box ROM paths. + # ======================================================== + # NVR/CMOS TEMPLATES (shipped with PCem distribution) + # Pre-configured BIOS settings. The emulator loads these on + # startup and writes back on shutdown. Without them the BIOS + # starts with blank CMOS settings. + # ======================================================== + - name: "NVR: Amstrad PC1512" + path: "pc1512.nvr" + required: false + note: "CMOS/NVRAM template for Amstrad PC1512." + source_ref: "src/nvr.c:390" + + - name: "NVR: Amstrad PC1640" + path: "pc1640.nvr" + required: false + note: "CMOS/NVRAM template for Amstrad PC1640." + source_ref: "src/nvr.c:391" + + - name: "NVR: Sinclair PC200" + path: "pc200.nvr" + required: false + note: "CMOS/NVRAM template for Sinclair PC200." + source_ref: "src/nvr.c:392" + + - name: "NVR: Amstrad PC2086" + path: "pc2086.nvr" + required: false + note: "CMOS/NVRAM template for Amstrad PC2086." + source_ref: "src/nvr.c:393" + + - name: "NVR: Amstrad PC3086" + path: "pc3086.nvr" + required: false + note: "CMOS/NVRAM template for Amstrad PC3086." + source_ref: "src/nvr.c:394" + + - name: "NVR: IBM AT" + path: "at.nvr" + required: false + note: "CMOS/NVRAM template for IBM AT." + source_ref: "src/nvr.c:395" + + - name: "NVR: IBM PS/1 model 2011" + path: "ibmps1_2011.nvr" + required: false + note: "CMOS/NVRAM template for IBM PS/1 model 2011." + source_ref: "src/nvr.c:396" + + - name: "NVR: Commodore PC 30 III" + path: "cmdpc30.nvr" + required: false + note: "CMOS/NVRAM template for Commodore PC 30 III." + source_ref: "src/nvr.c:397" + + - name: "NVR: AMI 286" + path: "ami286.nvr" + required: false + note: "CMOS/NVRAM template for AMI 286 clone." + source_ref: "src/nvr.c:398" + + - name: "NVR: Dell System 200" + path: "dell200.nvr" + required: false + note: "CMOS/NVRAM template for Dell System 200." + source_ref: "src/nvr.c:399" + + - name: "NVR: IBM AT 386" + path: "at386.nvr" + required: false + note: "CMOS/NVRAM template for IBM AT 386." + source_ref: "src/nvr.c:400" + + - name: "NVR: Compaq Deskpro 386" + path: "deskpro386.nvr" + required: false + note: "CMOS/NVRAM template for Compaq Deskpro 386." + source_ref: "src/nvr.c:401" + + - name: "NVR: Acer 386SX" + path: "acer386.nvr" + required: false + note: "CMOS/NVRAM template for Acer 386SX/25N." + source_ref: "src/nvr.c:402" + + - name: "NVR: Amstrad MegaPC" + path: "megapc.nvr" + required: false + note: "CMOS/NVRAM template for Amstrad MegaPC." + source_ref: "src/nvr.c:403" + + - name: "NVR: AMI 386" + path: "ami386.nvr" + required: false + note: "CMOS/NVRAM template for AMI 386 clone." + source_ref: "src/nvr.c:404" + + - name: "NVR: AMI 486" + path: "ami486.nvr" + required: false + note: "CMOS/NVRAM template for AMI 486 clone." + source_ref: "src/nvr.c:405" + + - name: "NVR: AMI WinBIOS 486" + path: "win486.nvr" + required: false + note: "CMOS/NVRAM template for AMI WinBIOS 486." + source_ref: "src/nvr.c:406" + + - name: "NVR: HOT-433 PCI 486" + path: "hot-433.nvr" + required: false + note: "CMOS/NVRAM template for HOT-433 PCI 486." + source_ref: "src/nvr.c:407" + + - name: "NVR: Award SiS 496" + path: "sis496.nvr" + required: false + note: "CMOS/NVRAM template for Award SiS 496/497." + source_ref: "src/nvr.c:408" + + - name: "NVR: Award 430VX" + path: "430vx.nvr" + required: false + note: "CMOS/NVRAM template for Award 430VX PCI." + source_ref: "src/nvr.c:409" + + - name: "NVR: Intel Premiere/PCI (Batman)" + path: "revenge.nvr" + required: false + note: "CMOS/NVRAM template for Intel Premiere/PCI." + source_ref: "src/nvr.c:410" + + - name: "NVR: Intel Advanced/EV (Endeavor)" + path: "endeavor.nvr" + required: false + note: "CMOS/NVRAM template for Intel Advanced/EV." + source_ref: "src/nvr.c:411" + + - name: "NVR: Phoenix 386" + path: "px386.nvr" + required: false + note: "CMOS/NVRAM template for Phoenix 386 clone." + source_ref: "src/nvr.c:412" + + - name: "NVR: DTK 386" + path: "dtk386.nvr" + required: false + note: "CMOS/NVRAM template for DTK 386SX clone." + source_ref: "src/nvr.c:413" + + - name: "NVR: DTK 486" + path: "dtk486.nvr" + required: false + note: "CMOS/NVRAM template for DTK 486." + source_ref: "src/nvr.c:414" + + - name: "NVR: Rise R418" + path: "r418.nvr" + required: false + note: "CMOS/NVRAM template for Rise Computer R418." + source_ref: "src/nvr.c:415" + + - name: "NVR: Intel Premiere/PCI II (Plato)" + path: "plato.nvr" + required: false + note: "CMOS/NVRAM template for Intel Premiere/PCI II." + source_ref: "src/nvr.c:416" + + - name: "NVR: PC Partner MB500N" + path: "mb500n.nvr" + required: false + note: "CMOS/NVRAM template for PC Partner MB500N." + source_ref: "src/nvr.c:417" + + - name: "NVR: Acer M3A" + path: "acerm3a.nvr" + required: false + note: "CMOS/NVRAM template for Acer M3A." + source_ref: "src/nvr.c:418" + + - name: "NVR: Acer V35N" + path: "acerv35n.nvr" + required: false + note: "CMOS/NVRAM template for Acer V35N." + source_ref: "src/nvr.c:419" + + - name: "NVR: ASUS P/I-P55T2P4" + path: "p55t2p4.nvr" + required: false + note: "CMOS/NVRAM template for ASUS P/I-P55T2P4." + source_ref: "src/nvr.c:420" + + - name: "NVR: Epox P55-VA" + path: "p55va.nvr" + required: false + note: "CMOS/NVRAM template for Epox P55-VA." + source_ref: "src/nvr.c:421" + + # ======================================================== + # EEPROM TEMPLATES (shipped with upstream PCem) + # Persistent settings for Tandy EEPROM and AdLib Gold. + # Loaded on startup, saved on shutdown. Without them the + # hardware starts with blank/default configuration. + # ======================================================== + - name: "Tandy 1000 HX EEPROM" + path: "tandy1000hx.bin" + required: false + note: "Tandy 1000 HX EEPROM state (128 bytes). Initializes to zeros if absent." + source_ref: "src/tandy_eeprom.c:128" + + - name: "Tandy 1000 SL/2 EEPROM" + path: "tandy1000sl2.bin" + required: false + note: "Tandy 1000 SL/2 EEPROM state (128 bytes). Initializes to zeros if absent." + source_ref: "src/tandy_eeprom.c:131" + + - name: "AdLib Gold EEPROM" + path: "nvr/adgold.bin" + required: false + note: "AdLib Gold sound card EEPROM state (24 bytes). Initializes to defaults if absent." + source_ref: "src/sound_adlibgold.c:789" diff --git a/emulators/pcsx1.yml b/emulators/pcsx1.yml index d4760023..c3a21f5a 100644 --- a/emulators/pcsx1.yml +++ b/emulators/pcsx1.yml @@ -1,8 +1,46 @@ -emulator: "pcsx1" -type: alias -alias_of: "pcsx_rearmed" -profiled_date: "2026-03-18" -core_version: "r21" +emulator: PCSX1 +type: libretro +core_classification: community_fork +source: "https://github.com/libretro/pcsx1-libretro" +upstream: "https://github.com/notaz/pcsx_rearmed" +profiled_date: "2026-03-24" +core_version: "r22" display_name: "Sony - PlayStation (PCSX1)" -note: "This core uses the same BIOS/firmware as pcsx_rearmed. See emulators/pcsx_rearmed.yml for details." -files: [] +cores: [pcsx1] +systems: [sony-playstation] +notes: > + Never-completed fork of PCSX-ReARMed targeting general PC hardware. Experimental, abandoned since 2018. + HLE BIOS built-in: if no BIOS file found, core warns and falls back to HLE automatically. + BIOS search order: scph1001.bin, scph5501.bin, scph7001.bin by exact filename, then scans system + directory for any file matching "scph*" (case-insensitive) with size == 512 KB. + No region detection, no hash validation. First valid BIOS found is used regardless of region. + .info declares scph5500/5501/5502 but code explicitly searches scph1001/5501/7001 first. + +files: + - name: "scph1001.bin" + description: "SCPH-1001 (v2.2 12-04-95 A)" + required: false + hle_fallback: true + size: 524288 + validation: [size] + source_ref: "frontend/libretro.c:1229 (bios[0]), :1181 (size check)" + + - name: "scph5501.bin" + description: "SCPH-5501 (v3.0 11-18-96 A)" + required: false + hle_fallback: true + size: 524288 + validation: [size] + source_ref: "frontend/libretro.c:1229 (bios[1]), :1181 (size check)" + + - name: "scph7001.bin" + description: "SCPH-7001 (v4.1 12-16-97 A)" + required: false + hle_fallback: true + size: 524288 + validation: [size] + source_ref: "frontend/libretro.c:1229 (bios[2]), :1181 (size check)" + + # Dynamic scan fallback: after the 3 named files, find_any_bios() scans the + # system directory for any file starting with "scph" (case-insensitive) that + # is exactly 512 KB. This covers scph5500.bin, scph5502.bin, scph3000.bin, etc. diff --git a/emulators/pcsx_rearmed.yml b/emulators/pcsx_rearmed.yml index 9710765f..7633d1eb 100644 --- a/emulators/pcsx_rearmed.yml +++ b/emulators/pcsx_rearmed.yml @@ -1,13 +1,13 @@ emulator: PCSX-ReARMed type: libretro +core_classification: embedded_hle source: "https://github.com/libretro/pcsx_rearmed" -profiled_date: "2026-03-18" -core_version: "r24" +upstream: "https://github.com/notaz/pcsx_rearmed" +profiled_date: "2026-03-24" +core_version: "r25" display_name: "Sony - PlayStation (PCSX ReARMed)" cores: [pcsx_rearmed, pcsx_rearmed_neon, pcsx_rearmed_interpreter] systems: [sony-playstation] -bios_size: 524288 # 512 KB (0x80000); also accepts 4 MB psxonpsp combo (reads first 512 KB) -verification: crc32 notes: > HLE BIOS built-in: core option pcsx_rearmed_bios = "HLE" bypasses real BIOS entirely. Default is "auto" which searches system dir for listed filenames, then scans all files. @@ -17,7 +17,6 @@ notes: > Files named "unirom" (case-insensitive) are explicitly skipped. Region fallback: if the matching-region BIOS is missing, any available region BIOS is used over HLE. Three region slots: US (index 0), JP (index 1), EU (index 2) stored in Config.Bios[]. - All firmware is optional per libretro-core-info; HLE works but real BIOS improves compatibility. files: # -- Region: Japan (PSX_REGION_JP = 1) -- @@ -25,10 +24,11 @@ files: - name: "scph5500.bin" description: "SCPH-5500 (v3.0 09-09-96 J)" region: "NTSC-J" + size: 524288 required: false hle_fallback: true - md5: "8dd7d5296a650fac7319bce665a6a53c" - source_ref: "frontend/libretro.c:3710 (listed_bios[0])" + validation: [size] + source_ref: "frontend/libretro.c:3721-3724 (listed_bios[0])" notes: "Preferred JP BIOS, searched first by exact filename." # -- Region: North America (PSX_REGION_US = 0) -- @@ -36,10 +36,11 @@ files: - name: "scph5501.bin" description: "SCPH-5501 (v3.0 11-18-96 A)" region: "NTSC-U" + size: 524288 required: false hle_fallback: true - md5: "490f666e1afb15b7362b406ed1cea246" - source_ref: "frontend/libretro.c:3710 (listed_bios[1])" + validation: [size] + source_ref: "frontend/libretro.c:3721-3724 (listed_bios[1])" notes: "Preferred US BIOS, searched first by exact filename." # -- Region: Europe (PSX_REGION_EU = 2) -- @@ -47,48 +48,52 @@ files: - name: "scph5502.bin" description: "SCPH-5502 (v3.0 01-06-97 E)" region: "PAL" + size: 524288 required: false hle_fallback: true - md5: "32736f17079d0b2b7024407c39bd3050" - source_ref: "frontend/libretro.c:3710 (listed_bios[2])" + validation: [size] + source_ref: "frontend/libretro.c:3721-3724 (listed_bios[2])" notes: "Preferred EU BIOS, searched first by exact filename." # -- Fallback BIOS filenames (searched in order after scph550x) -- - name: "psxonpsp660.bin" - description: "PSP embedded PS1 BIOS (region-free, 512 KB or 4 MB accepted)" + description: "PSP embedded PS1 BIOS (region detected at runtime)" region: "Auto" + size: 524288 required: false hle_fallback: true - md5: "c53ca5908936d412331790f4426c6c33" - source_ref: "frontend/libretro.c:3711 (listed_bios[3])" - notes: "Region detected from content at runtime. 4 MB combo image accepted but only first 512 KB read." + validation: [size] + source_ref: "frontend/libretro.c:3721-3724 (listed_bios[3])" + notes: "Region detected from content at runtime. Must be 512 KB in libretro context." - name: "scph101.bin" description: "SCPH-101 (v4.4 03-24-00 A) - PSone US" region: "NTSC-U" + size: 524288 required: false hle_fallback: true - md5: "6e3735ff4c7dc899ee98981c18c3666d" - source_ref: "frontend/libretro.c:3711 (listed_bios[4])" - notes: "PSone slim model. Searched by filename after scph550x and psxonpsp660." + validation: [size] + source_ref: "frontend/libretro.c:3721-3724 (listed_bios[4])" + notes: "PSone slim model." - name: "scph7001.bin" description: "SCPH-7001 (v4.1 12-16-97 A)" region: "NTSC-U" + size: 524288 required: false hle_fallback: true - md5: "1e68c231d0896b7eadcad1d7d8e76129" - source_ref: "frontend/libretro.c:3711 (listed_bios[5])" - notes: "Searched by filename after psxonpsp660 and scph101." + validation: [size] + source_ref: "frontend/libretro.c:3721-3724 (listed_bios[5])" - name: "scph1001.bin" description: "SCPH-1001 (v2.2 12-04-95 A)" region: "NTSC-U" + size: 524288 required: false hle_fallback: true - md5: "924e392ed05558ffdb115408c263dccf" - source_ref: "frontend/libretro.c:3711 (listed_bios[6])" + validation: [size] + source_ref: "frontend/libretro.c:3721-3724 (listed_bios[6])" notes: "Original US model. Last in the explicit filename search list." # -- Dynamic scan fallback -- diff --git a/emulators/picodrive.yml b/emulators/picodrive.yml index 1e98545f..b5e9a8b2 100644 --- a/emulators/picodrive.yml +++ b/emulators/picodrive.yml @@ -1,9 +1,12 @@ emulator: PicoDrive type: libretro +core_classification: official_port source: "https://github.com/libretro/picodrive" -profiled_date: "2026-03-18" +upstream: "https://github.com/irixxxx/picodrive" +profiled_date: "2026-03-24" core_version: "1.99" display_name: "Sega - MS/GG/MD/CD/32X (PicoDrive)" +cores: [picodrive] systems: - sega-megadrive - sega-genesis @@ -13,33 +16,48 @@ systems: - sega-mastersystem - sega-gamegear - sega-sg1000 + - sega-sc3000 - sega-pico notes: | PicoDrive is a fast Mega Drive / Genesis emulator with Mega CD, 32X, Master - System, Game Gear, SG-1000 and Sega Pico support. + System, Game Gear, SG-1000, SC-3000 and Sega Pico support. The libretro port + is part of the PicoDrive source tree (platform/libretro/). Mega CD / Sega CD games require a region-matching BIOS file. The core searches the system directory for each name in order, trying .bin then .zip extension, and uses the first file found. If no BIOS is found, CD games fail to load with PM_BAD_CD_NO_BIOS. MSU-MD games can run without BIOS. - BIOS filename search order (platform/libretro/libretro.c:1265-1329): + BIOS filename search order (platform/libretro/libretro.c:1264-1328): US: us_scd2_9306, SegaCDBIOS9303, us_scd1_9210, bios_CD_U EU: eu_mcd2_9306, eu_mcd2_9303, eu_mcd1_9210, bios_CD_E JP: jp_mcd2_921222, jp_mcd1_9112, jp_mcd1_9111, bios_CD_J - 32X BIOS files (m68k, master SH2, slave SH2) are fully optional. The core - has built-in HLE that generates replacement code at startup when the external - BIOS pointers are NULL (pico/32x/memory.c:2200 get_bios(), pico/32x/32x.c:172). - The libretro frontend does not expose any 32X BIOS loading path. Only the - standalone platform code references 32X_M_BIOS.BIN / 32X_S_BIOS.BIN, and - that code is currently disabled (#if 0 in platform/common/emu.c:1529). + 32X BIOS files (m68k, master SH2, slave SH2) are fully optional. The engine + has built-in HLE that generates replacement code at startup when the BIOS + pointers are NULL (pico/32x/memory.c:2199 get_bios(), pico/32x/32x.c:172). + No current frontend (libretro or standalone) loads these files; the standalone + loading code is disabled (#if 0 in platform/common/emu.c:1529). - Master System, Game Gear, SG-1000: no BIOS file loaded. The core initializes - VDP registers and RAM to simulate post-BIOS state (pico/sms.c:1080-1096). + carthw.cfg is a cartridge hardware database loaded from the system directory. + A built-in version is compiled into the core (pico/carthw_cfg.c). The external + file supplements or overrides game-specific hardware detection (SVP, EEPROM + types, special mappers, copy protection). + + Master System, Game Gear, SG-1000, SC-3000: no BIOS file loaded. The core + initializes VDP registers and RAM to simulate post-BIOS state + (pico/sms.c:1080-1097). files: + # ------------------------------------------------------- + # Cartridge hardware database + # ------------------------------------------------------- + - name: "carthw.cfg" + required: false + note: "Cartridge hardware database for special mapper and EEPROM detection. Built-in fallback compiled into the core." + source_ref: "platform/libretro/libretro.c:1619, pico/cart.c:1020-1022" + # ------------------------------------------------------- # Mega CD / Sega CD - US region # ------------------------------------------------------- @@ -48,28 +66,28 @@ files: required: true size: 131072 # 128 KB (0x20000) note: "US Sega CD Model 2 BIOS (September 1993). First in US search order." - source_ref: "platform/libretro/libretro.c:1266" + source_ref: "platform/libretro/libretro.c:1265" - name: "SegaCDBIOS9303.bin" system: sega-segacd required: false size: 131072 note: "US Sega CD BIOS (March 1993). Second in US search order." - source_ref: "platform/libretro/libretro.c:1266" + source_ref: "platform/libretro/libretro.c:1265" - name: "us_scd1_9210.bin" system: sega-segacd required: false size: 131072 note: "US Sega CD Model 1 BIOS (October 1992). Third in US search order." - source_ref: "platform/libretro/libretro.c:1266" + source_ref: "platform/libretro/libretro.c:1265" - name: "bios_CD_U.bin" system: sega-segacd required: false size: 131072 note: "US Sega CD BIOS (generic name). Last in US search order." - source_ref: "platform/libretro/libretro.c:1266" + source_ref: "platform/libretro/libretro.c:1265" # ------------------------------------------------------- # Mega CD / Sega CD - EU region @@ -79,28 +97,28 @@ files: required: true size: 131072 note: "EU Mega CD Model 2 BIOS (June 1993). First in EU search order." - source_ref: "platform/libretro/libretro.c:1269" + source_ref: "platform/libretro/libretro.c:1268" - name: "eu_mcd2_9303.bin" system: sega-megacd required: false size: 131072 note: "EU Mega CD Model 2 BIOS (March 1993). Second in EU search order." - source_ref: "platform/libretro/libretro.c:1269" + source_ref: "platform/libretro/libretro.c:1268" - name: "eu_mcd1_9210.bin" system: sega-megacd required: false size: 131072 note: "EU Mega CD Model 1 BIOS (October 1992). Third in EU search order." - source_ref: "platform/libretro/libretro.c:1269" + source_ref: "platform/libretro/libretro.c:1268" - name: "bios_CD_E.bin" system: sega-megacd required: false size: 131072 note: "EU Mega CD BIOS (generic name). Last in EU search order." - source_ref: "platform/libretro/libretro.c:1269" + source_ref: "platform/libretro/libretro.c:1268" # ------------------------------------------------------- # Mega CD / Sega CD - JP region @@ -110,31 +128,31 @@ files: required: true size: 131072 note: "JP Mega CD Model 2 BIOS (December 1992). First in JP search order." - source_ref: "platform/libretro/libretro.c:1272" + source_ref: "platform/libretro/libretro.c:1271" - name: "jp_mcd1_9112.bin" system: sega-megacd required: false size: 131072 note: "JP Mega CD Model 1 BIOS (December 1991). Second in JP search order." - source_ref: "platform/libretro/libretro.c:1272" + source_ref: "platform/libretro/libretro.c:1271" - name: "jp_mcd1_9111.bin" system: sega-megacd required: false size: 131072 note: "JP Mega CD Model 1 BIOS (November 1991). Third in JP search order." - source_ref: "platform/libretro/libretro.c:1272" + source_ref: "platform/libretro/libretro.c:1271" - name: "bios_CD_J.bin" system: sega-megacd required: false size: 131072 note: "JP Mega CD BIOS (generic name). Last in JP search order." - source_ref: "platform/libretro/libretro.c:1272" + source_ref: "platform/libretro/libretro.c:1271" # ------------------------------------------------------- - # Sega 32X - HLE available, not loaded by libretro frontend + # Sega 32X - HLE available, no frontend loads these # ------------------------------------------------------- - name: "32X_G_BIOS.BIN" system: sega-32x @@ -142,7 +160,7 @@ files: hle_fallback: true size: 256 # 0x100 note: "32X 68K (Genesis-side) BIOS. HLE replacement generated when NULL." - source_ref: "pico/32x/memory.c:2207-2243" + source_ref: "pico/32x/memory.c:2206-2243" - name: "32X_M_BIOS.BIN" system: sega-32x @@ -150,7 +168,7 @@ files: hle_fallback: true size: 2048 # 0x800 note: "32X Master SH2 BIOS. HLE replacement generated when NULL." - source_ref: "pico/32x/memory.c:2250-2277" + source_ref: "pico/32x/memory.c:2249-2276" - name: "32X_S_BIOS.BIN" system: sega-32x @@ -158,7 +176,7 @@ files: hle_fallback: true size: 1024 # 0x400 note: "32X Slave SH2 BIOS. HLE replacement generated when NULL." - source_ref: "pico/32x/memory.c:2280-2298" + source_ref: "pico/32x/memory.c:2279-2297" platform_details: megacd: @@ -166,16 +184,16 @@ platform_details: hle_available: false region_specific: true extensions_tried: [".bin", ".zip"] - source_ref: "pico/pico_int.h:559, platform/libretro/libretro.c:1310-1318" + source_ref: "pico/pico_int.h:559, platform/libretro/libretro.c:1309-1317" 32x: m68k_bios_size: 256 # 0x100 master_sh2_bios_size: 2048 # 0x800 slave_sh2_bios_size: 1024 # 0x400 hle_available: true - source_ref: "pico/pico.h:53-55, pico/pico_int.h:679-693" + source_ref: "pico/pico.h:54-55, pico/pico_int.h:679-693" sms: hle_available: true note: "No BIOS file loaded. VDP/RAM initialized to post-BIOS state." - source_ref: "pico/sms.c:1080-1096" + source_ref: "pico/sms.c:1080-1097" diff --git a/emulators/pokemini.yml b/emulators/pokemini.yml index 3aa8979e..f55e5815 100644 --- a/emulators/pokemini.yml +++ b/emulators/pokemini.yml @@ -1,7 +1,9 @@ emulator: PokeMini type: libretro +core_classification: community_fork source: "https://github.com/libretro/PokeMini" -profiled_date: "2026-03-18" +upstream: "https://sourceforge.net/projects/pokemini/" +profiled_date: "2026-03-24" core_version: "v0.60" display_name: "Nintendo - Pokemon Mini (PokeMini)" cores: @@ -56,8 +58,6 @@ files: required: false hle_fallback: true size: 4096 - md5: "1e4fb124a3a886865acb574f388c803d" - sha1: "daad4113713ed776fbd47727762bca81ba74915f" source_ref: "source/PokeMini.c:189-206 (PokeMini_LoadBIOSFile), libretro/libretro.c:565 (path)" notes: "Mapped at $000000-$000FFF (4 KB). Read via Hardware.c:144-145. Falls back to embedded FreeBIOS if missing." diff --git a/scripts/common.py b/scripts/common.py index e5f6810c..acad4ce6 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -79,12 +79,20 @@ def md5_composite(filepath: str | Path) -> str: names = sorted(n for n in zf.namelist() if not n.endswith("/")) h = hashlib.md5() for name in names: + info = zf.getinfo(name) + if info.file_size > 512 * 1024 * 1024: + continue # skip oversized entries h.update(zf.read(name)) result = h.hexdigest() _md5_composite_cache[key] = result return result +def parse_md5_list(raw: str) -> list[str]: + """Parse comma-separated MD5 string into normalized lowercase list.""" + return [m.strip().lower() for m in raw.split(",") if m.strip()] if raw else [] + + def load_platform_config(platform_name: str, platforms_dir: str = "platforms") -> dict: """Load a platform config with inheritance and shared group resolution. @@ -162,6 +170,7 @@ def resolve_local_file( db: dict, zip_contents: dict | None = None, dest_hint: str = "", + _depth: int = 0, ) -> tuple[str | None, str]: """Resolve a BIOS file to its local path using database.json. @@ -293,13 +302,16 @@ def resolve_local_file( return path, "zip_exact" # MAME clone fallback: if a file was deduped, resolve via canonical - clone_map = _get_mame_clone_map() - canonical = clone_map.get(name) - if canonical and canonical != name: - canonical_entry = {"name": canonical} - result = resolve_local_file(canonical_entry, db, zip_contents, dest_hint) - if result[0]: - return result[0], "mame_clone" + if _depth < 3: + clone_map = _get_mame_clone_map() + canonical = clone_map.get(name) + if canonical and canonical != name: + canonical_entry = {"name": canonical} + result = resolve_local_file( + canonical_entry, db, zip_contents, dest_hint, _depth=_depth + 1, + ) + if result[0]: + return result[0], "mame_clone" return None, "not_found" @@ -333,6 +345,9 @@ def check_inside_zip(container: str, file_name: str, expected_md5: str) -> str: with zipfile.ZipFile(container) as archive: for fname in archive.namelist(): if fname.casefold() == file_name.casefold(): + info = archive.getinfo(fname) + if info.file_size > 512 * 1024 * 1024: + return "error" if expected_md5 == "": return "ok" with archive.open(fname) as entry: @@ -365,10 +380,16 @@ def build_zip_contents_index(db: dict, max_entry_size: int = 512 * 1024 * 1024) return index +_emulator_profiles_cache: dict[tuple[str, bool], dict[str, dict]] = {} + + def load_emulator_profiles( emulators_dir: str, skip_aliases: bool = True, ) -> dict[str, dict]: - """Load all emulator YAML profiles from a directory.""" + """Load all emulator YAML profiles from a directory (cached).""" + cache_key = (os.path.realpath(emulators_dir), skip_aliases) + if cache_key in _emulator_profiles_cache: + return _emulator_profiles_cache[cache_key] try: import yaml except ImportError: @@ -385,6 +406,7 @@ def load_emulator_profiles( if skip_aliases and profile.get("type") == "alias": continue profiles[f.stem] = profile + _emulator_profiles_cache[cache_key] = profiles return profiles @@ -461,6 +483,192 @@ def resolve_platform_cores( } +def _parse_validation(validation: list | dict | None) -> list[str]: + """Extract the validation check list from a file's validation field. + + Handles both simple list and divergent (core/upstream) dict forms. + For dicts, uses the ``core`` key since RetroArch users run the core. + """ + if validation is None: + return [] + if isinstance(validation, list): + return validation + if isinstance(validation, dict): + return validation.get("core", []) + return [] + + +# Validation types that require console-specific cryptographic keys. +# verify.py cannot reproduce these — size checks still apply if combined. +_CRYPTO_CHECKS = frozenset({"signature", "crypto"}) + +# All reproducible validation types. +_HASH_CHECKS = frozenset({"crc32", "md5", "sha1", "adler32"}) + + +def _build_validation_index(profiles: dict) -> dict[str, dict]: + """Build per-filename validation rules from emulator profiles. + + Returns {filename: {"checks": [str], "size": int|None, "min_size": int|None, + "max_size": int|None, "crc32": str|None, "md5": str|None, "sha1": str|None, + "adler32": str|None, "crypto_only": [str]}}. + + ``crypto_only`` lists validation types we cannot reproduce (signature, crypto) + so callers can report them as non-verifiable rather than silently skipping. + + When multiple emulators reference the same file, merges checks (union). + Raises ValueError if two profiles declare conflicting values. + """ + index: dict[str, dict] = {} + sources: dict[str, dict[str, str]] = {} + for emu_name, profile in profiles.items(): + if profile.get("type") in ("launcher", "alias"): + continue + for f in profile.get("files", []): + fname = f.get("name", "") + if not fname: + continue + checks = _parse_validation(f.get("validation")) + if not checks: + continue + if fname not in index: + index[fname] = { + "checks": set(), "sizes": set(), + "min_size": None, "max_size": None, + "crc32": set(), "md5": set(), "sha1": set(), "sha256": set(), + "adler32": set(), "crypto_only": set(), + } + sources[fname] = {} + index[fname]["checks"].update(checks) + # Track non-reproducible crypto checks + index[fname]["crypto_only"].update( + c for c in checks if c in _CRYPTO_CHECKS + ) + # Size checks + if "size" in checks: + if f.get("size") is not None: + index[fname]["sizes"].add(f["size"]) + if f.get("min_size") is not None: + cur = index[fname]["min_size"] + index[fname]["min_size"] = min(cur, f["min_size"]) if cur is not None else f["min_size"] + if f.get("max_size") is not None: + cur = index[fname]["max_size"] + index[fname]["max_size"] = max(cur, f["max_size"]) if cur is not None else f["max_size"] + # Hash checks — collect all accepted hashes as sets (multiple valid + # versions of the same file, e.g. MT-32 ROM versions) + if "crc32" in checks and f.get("crc32"): + norm = f["crc32"].lower() + if norm.startswith("0x"): + norm = norm[2:] + index[fname]["crc32"].add(norm) + for hash_type in ("md5", "sha1", "sha256"): + if hash_type in checks and f.get(hash_type): + index[fname][hash_type].add(f[hash_type].lower()) + # Adler32 — stored as known_hash_adler32 field (not in validation: list + # for Dolphin, but support it in both forms for future profiles) + adler_val = f.get("known_hash_adler32") or f.get("adler32") + if adler_val: + norm = adler_val.lower() + if norm.startswith("0x"): + norm = norm[2:] + index[fname]["adler32"].add(norm) + # Convert sets to sorted tuples/lists for determinism + for v in index.values(): + v["checks"] = sorted(v["checks"]) + v["crypto_only"] = sorted(v["crypto_only"]) + # Keep hash sets as frozensets for O(1) lookup in check_file_validation + return index + + +def check_file_validation( + local_path: str, filename: str, validation_index: dict[str, dict], + bios_dir: str = "bios", +) -> str | None: + """Check emulator-level validation on a resolved file. + + Supports: size (exact/min/max), crc32, md5, sha1, adler32, + signature (RSA-2048 PKCS1v15 SHA256), crypto (AES-128-CBC + SHA256). + + Returns None if all checks pass or no validation applies. + Returns a reason string if a check fails. + """ + entry = validation_index.get(filename) + if not entry: + return None + checks = entry["checks"] + + # Size checks — sizes is a set of accepted values + if "size" in checks: + actual_size = os.path.getsize(local_path) + if entry["sizes"] and actual_size not in entry["sizes"]: + expected = ",".join(str(s) for s in sorted(entry["sizes"])) + return f"size mismatch: got {actual_size}, accepted [{expected}]" + if entry["min_size"] is not None and actual_size < entry["min_size"]: + return f"size too small: min {entry['min_size']}, got {actual_size}" + if entry["max_size"] is not None and actual_size > entry["max_size"]: + return f"size too large: max {entry['max_size']}, got {actual_size}" + + # Hash checks — compute once, reuse for all hash types. + # Each hash field is a set of accepted values (multiple valid ROM versions). + need_hashes = ( + any(h in checks and entry.get(h) for h in ("crc32", "md5", "sha1", "sha256")) + or entry.get("adler32") + ) + if need_hashes: + hashes = compute_hashes(local_path) + if "crc32" in checks and entry["crc32"]: + if hashes["crc32"].lower() not in entry["crc32"]: + expected = ",".join(sorted(entry["crc32"])) + return f"crc32 mismatch: got {hashes['crc32']}, accepted [{expected}]" + if "md5" in checks and entry["md5"]: + if hashes["md5"].lower() not in entry["md5"]: + expected = ",".join(sorted(entry["md5"])) + return f"md5 mismatch: got {hashes['md5']}, accepted [{expected}]" + if "sha1" in checks and entry["sha1"]: + if hashes["sha1"].lower() not in entry["sha1"]: + expected = ",".join(sorted(entry["sha1"])) + return f"sha1 mismatch: got {hashes['sha1']}, accepted [{expected}]" + if "sha256" in checks and entry["sha256"]: + if hashes["sha256"].lower() not in entry["sha256"]: + expected = ",".join(sorted(entry["sha256"])) + return f"sha256 mismatch: got {hashes['sha256']}, accepted [{expected}]" + if entry["adler32"]: + if hashes["adler32"].lower() not in entry["adler32"]: + expected = ",".join(sorted(entry["adler32"])) + return f"adler32 mismatch: got 0x{hashes['adler32']}, accepted [{expected}]" + + # Signature/crypto checks (3DS RSA, AES) + if entry["crypto_only"]: + from crypto_verify import check_crypto_validation + crypto_reason = check_crypto_validation(local_path, filename, bios_dir) + if crypto_reason: + return crypto_reason + + return None + + +def validate_cli_modes(args, mode_attrs: list[str]) -> None: + """Validate mutual exclusion of CLI mode arguments.""" + modes = sum(1 for attr in mode_attrs if getattr(args, attr, None)) + if modes == 0: + raise SystemExit(f"Specify one of: --{' --'.join(mode_attrs)}") + if modes > 1: + raise SystemExit(f"Options are mutually exclusive: --{' --'.join(mode_attrs)}") + + +def filter_files_by_mode(files: list[dict], standalone: bool) -> list[dict]: + """Filter file entries by libretro/standalone mode.""" + result = [] + for f in files: + fmode = f.get("mode", "") + if standalone and fmode == "libretro": + continue + if not standalone and fmode == "standalone": + continue + result.append(f) + return result + + def safe_extract_zip(zip_path: str, dest_dir: str) -> None: """Extract a ZIP file safely, preventing zip-slip path traversal.""" dest = os.path.realpath(dest_dir) @@ -470,3 +678,31 @@ def safe_extract_zip(zip_path: str, dest_dir: str) -> None: if not member_path.startswith(dest + os.sep) and member_path != dest: raise ValueError(f"Zip slip detected: {member.filename}") zf.extract(member, dest) + + +def list_emulator_profiles(emulators_dir: str, skip_aliases: bool = True) -> None: + """Print available emulator profiles.""" + profiles = load_emulator_profiles(emulators_dir, skip_aliases=False) + for name in sorted(profiles): + p = profiles[name] + if p.get("type") in ("alias", "test"): + continue + display = p.get("emulator", name) + ptype = p.get("type", "libretro") + systems = ", ".join(p.get("systems", [])[:3]) + more = "..." if len(p.get("systems", [])) > 3 else "" + print(f" {name:30s} {display:40s} [{ptype}] {systems}{more}") + + +def list_system_ids(emulators_dir: str) -> None: + """Print available system IDs with emulator count.""" + profiles = load_emulator_profiles(emulators_dir) + system_emus: dict[str, list[str]] = {} + for name, p in profiles.items(): + if p.get("type") in ("alias", "test", "launcher"): + continue + for sys_id in p.get("systems", []): + system_emus.setdefault(sys_id, []).append(name) + for sys_id in sorted(system_emus): + count = len(system_emus[sys_id]) + print(f" {sys_id:35s} ({count} emulator{'s' if count > 1 else ''})") diff --git a/scripts/crypto_verify.py b/scripts/crypto_verify.py index b7c8ebb5..9074863d 100644 --- a/scripts/crypto_verify.py +++ b/scripts/crypto_verify.py @@ -19,6 +19,7 @@ from __future__ import annotations import hashlib import struct import subprocess +from collections.abc import Callable from pathlib import Path @@ -418,7 +419,7 @@ def verify_otp( # --------------------------------------------------------------------------- # Map from (filename, validation_type) to verification function -_CRYPTO_VERIFIERS: dict[str, callable] = { +_CRYPTO_VERIFIERS: dict[str, Callable] = { "SecureInfo_A": verify_secure_info_a, "LocalFriendCodeSeed_B": verify_local_friend_code_seed_b, "movable.sed": verify_movable_sed, diff --git a/scripts/generate_pack.py b/scripts/generate_pack.py index 1ee40e11..f32803f3 100644 --- a/scripts/generate_pack.py +++ b/scripts/generate_pack.py @@ -25,10 +25,11 @@ from pathlib import Path sys.path.insert(0, os.path.dirname(__file__)) from common import ( - build_zip_contents_index, check_inside_zip, compute_hashes, - group_identical_platforms, load_database, load_data_dir_registry, - load_emulator_profiles, load_platform_config, md5_composite, - resolve_local_file, + _build_validation_index, build_zip_contents_index, check_file_validation, + check_inside_zip, compute_hashes, filter_files_by_mode, + group_identical_platforms, list_emulator_profiles, list_system_ids, + load_database, load_data_dir_registry, load_emulator_profiles, + load_platform_config, md5_composite, resolve_local_file, ) from deterministic_zip import rebuild_zip_deterministic @@ -256,7 +257,6 @@ def generate_pack( file_reasons: dict[str, str] = {} # Build emulator-level validation index (same as verify.py) - from verify import _build_validation_index validation_index = {} if emu_profiles: validation_index = _build_validation_index(emu_profiles) @@ -367,7 +367,6 @@ def generate_pack( # In md5 mode: validation downgrades OK to UNTESTED if (file_status.get(dedup_key) == "ok" and local_path and validation_index): - from verify import check_file_validation fname = file_entry.get("name", "") reason = check_file_validation(local_path, fname, validation_index) if reason: @@ -523,19 +522,6 @@ def _normalize_zip_for_pack(source_zip: str, dest_path: str, target_zf: zipfile. # Emulator/system mode pack generation # --------------------------------------------------------------------------- -def _filter_files_by_mode(files: list[dict], standalone: bool) -> list[dict]: - """Filter file entries by libretro/standalone mode.""" - result = [] - for f in files: - fmode = f.get("mode", "") - if standalone and fmode == "libretro": - continue - if not standalone and fmode == "standalone": - continue - result.append(f) - return result - - def _resolve_destination(file_entry: dict, pack_structure: dict | None, standalone: bool) -> str: """Resolve the ZIP destination path for a file entry.""" @@ -620,7 +606,7 @@ def generate_emulator_pack( with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf: for emu_name, profile in sorted(selected): pack_structure = profile.get("pack_structure") - files = _filter_files_by_mode(profile.get("files", []), standalone) + files = filter_files_by_mode(profile.get("files", []), standalone) for dd in profile.get("data_directories", []): ref_key = dd.get("ref", "") if not ref_key or not data_registry or ref_key not in data_registry: @@ -825,34 +811,6 @@ def generate_system_pack( return result -def _list_emulators_pack(emulators_dir: str) -> None: - """Print available emulator profiles for pack generation.""" - profiles = load_emulator_profiles(emulators_dir, skip_aliases=False) - for name in sorted(profiles): - p = profiles[name] - if p.get("type") in ("alias", "test"): - continue - display = p.get("emulator", name) - ptype = p.get("type", "libretro") - systems = ", ".join(p.get("systems", [])[:3]) - more = "..." if len(p.get("systems", [])) > 3 else "" - print(f" {name:30s} {display:40s} [{ptype}] {systems}{more}") - - -def _list_systems_pack(emulators_dir: str) -> None: - """Print available system IDs with emulator count.""" - profiles = load_emulator_profiles(emulators_dir) - system_emus: dict[str, list[str]] = {} - for name, p in profiles.items(): - if p.get("type") in ("alias", "test", "launcher"): - continue - for sys_id in p.get("systems", []): - system_emus.setdefault(sys_id, []).append(name) - for sys_id in sorted(system_emus): - count = len(system_emus[sys_id]) - print(f" {sys_id:35s} ({count} emulator{'s' if count > 1 else ''})") - - def list_platforms(platforms_dir: str) -> list[str]: """List available platform names from YAML files.""" platforms = [] @@ -893,10 +851,10 @@ def main(): print(p) return if args.list_emulators: - _list_emulators_pack(args.emulators_dir) + list_emulator_profiles(args.emulators_dir) return if args.list_systems: - _list_systems_pack(args.emulators_dir) + list_system_ids(args.emulators_dir) return # Mutual exclusion @@ -1022,10 +980,15 @@ def verify_pack(zip_path: str, db: dict) -> tuple[bool, dict]: if name.startswith("INSTRUCTIONS_") or name == "manifest.json": continue with zf.open(info) as f: - data = f.read() - sha1 = hashlib.sha1(data).hexdigest() - md5 = hashlib.md5(data).hexdigest() - size = len(data) + sha1_h = hashlib.sha1() + md5_h = hashlib.md5() + size = 0 + for chunk in iter(lambda: f.read(65536), b""): + sha1_h.update(chunk) + md5_h.update(chunk) + size += len(chunk) + sha1 = sha1_h.hexdigest() + md5 = md5_h.hexdigest() # Look up in database: files_db keyed by SHA1 db_entry = files_db.get(sha1) @@ -1080,25 +1043,33 @@ def verify_pack(zip_path: str, db: dict) -> tuple[bool, dict]: def inject_manifest(zip_path: str, manifest: dict) -> None: """Inject manifest.json into an existing ZIP pack.""" - import tempfile as _tempfile manifest_json = json.dumps(manifest, indent=2, ensure_ascii=False) - # ZipFile doesn't support appending to existing entries, - # so we rebuild with the manifest added - tmp_fd, tmp_path = _tempfile.mkstemp(suffix=".zip", dir=os.path.dirname(zip_path)) - os.close(tmp_fd) - try: - with zipfile.ZipFile(zip_path, "r") as src, \ - zipfile.ZipFile(tmp_path, "w", zipfile.ZIP_DEFLATED) as dst: - for item in src.infolist(): - if item.filename == "manifest.json": - continue # replace existing - dst.writestr(item, src.read(item.filename)) - dst.writestr("manifest.json", manifest_json) - os.replace(tmp_path, zip_path) - except Exception: - os.unlink(tmp_path) - raise + # Check if manifest already exists + with zipfile.ZipFile(zip_path, "r") as zf: + has_manifest = "manifest.json" in zf.namelist() + + if not has_manifest: + # Fast path: append directly + with zipfile.ZipFile(zip_path, "a") as zf: + zf.writestr("manifest.json", manifest_json) + else: + # Rebuild to replace existing manifest + import tempfile as _tempfile + tmp_fd, tmp_path = _tempfile.mkstemp(suffix=".zip", dir=os.path.dirname(zip_path)) + os.close(tmp_fd) + try: + with zipfile.ZipFile(zip_path, "r") as src, \ + zipfile.ZipFile(tmp_path, "w", zipfile.ZIP_DEFLATED) as dst: + for item in src.infolist(): + if item.filename == "manifest.json": + continue + dst.writestr(item, src.read(item.filename)) + dst.writestr("manifest.json", manifest_json) + os.replace(tmp_path, zip_path) + except (OSError, zipfile.BadZipFile): + os.unlink(tmp_path) + raise def generate_sha256sums(output_dir: str) -> str | None: diff --git a/scripts/refresh_data_dirs.py b/scripts/refresh_data_dirs.py index 283be601..5893a864 100644 --- a/scripts/refresh_data_dirs.py +++ b/scripts/refresh_data_dirs.py @@ -198,11 +198,22 @@ def _download_and_extract( shutil.copyfileobj(src, dst) file_count += 1 - # atomic swap: remove old cache, move new into place - if cache_dir.exists(): - shutil.rmtree(cache_dir) + # atomic swap: rename old before moving new into place cache_dir.parent.mkdir(parents=True, exist_ok=True) - shutil.move(str(extract_dir), str(cache_dir)) + old_cache = cache_dir.with_suffix(".old") + if cache_dir.exists(): + if old_cache.exists(): + shutil.rmtree(old_cache) + cache_dir.rename(old_cache) + try: + shutil.move(str(extract_dir), str(cache_dir)) + except OSError: + # Restore old cache on failure + if old_cache.exists() and not cache_dir.exists(): + old_cache.rename(cache_dir) + raise + if old_cache.exists(): + shutil.rmtree(old_cache) return file_count diff --git a/scripts/scraper/coreinfo_scraper.py b/scripts/scraper/coreinfo_scraper.py index 4c9ff1a1..15891e5b 100644 --- a/scripts/scraper/coreinfo_scraper.py +++ b/scripts/scraper/coreinfo_scraper.py @@ -194,6 +194,7 @@ class Scraper(BaseScraper): """Scraper for libretro-core-info firmware declarations.""" def __init__(self): + super().__init__() self._info_files: dict[str, dict] | None = None def _fetch_info_list(self) -> list[str]: diff --git a/scripts/scraper/emudeck_scraper.py b/scripts/scraper/emudeck_scraper.py index 52560c58..68d712c5 100644 --- a/scripts/scraper/emudeck_scraper.py +++ b/scripts/scraper/emudeck_scraper.py @@ -185,6 +185,7 @@ class Scraper(BaseScraper): """Scraper for EmuDeck checkBIOS.sh and CSV cheat sheets.""" def __init__(self, checkbios_url: str = CHECKBIOS_URL, csv_base_url: str = CSV_BASE_URL): + super().__init__(url=checkbios_url) self.checkbios_url = checkbios_url self.csv_base_url = csv_base_url self._raw_script: str | None = None diff --git a/scripts/validate_pr.py b/scripts/validate_pr.py index 7d048d33..c1dbc0e6 100644 --- a/scripts/validate_pr.py +++ b/scripts/validate_pr.py @@ -93,7 +93,10 @@ class ValidationResult: def load_database(db_path: str) -> dict | None: try: return _load_database(db_path) - except (FileNotFoundError, json.JSONDecodeError): + except FileNotFoundError: + return None + except json.JSONDecodeError as e: + print(f"WARNING: corrupt database.json: {e}", file=sys.stderr) return None diff --git a/scripts/verify.py b/scripts/verify.py index 76d162c8..fc009b2b 100644 --- a/scripts/verify.py +++ b/scripts/verify.py @@ -35,13 +35,12 @@ except ImportError: sys.path.insert(0, os.path.dirname(__file__)) from common import ( - build_zip_contents_index, check_inside_zip, compute_hashes, - group_identical_platforms, load_data_dir_registry, - load_emulator_profiles, load_platform_config, + _build_validation_index, build_zip_contents_index, check_file_validation, + check_inside_zip, compute_hashes, filter_files_by_mode, + group_identical_platforms, list_emulator_profiles, list_system_ids, + load_data_dir_registry, load_emulator_profiles, load_platform_config, md5sum, md5_composite, resolve_local_file, resolve_platform_cores, ) -from crypto_verify import check_crypto_validation - DEFAULT_DB = "database.json" DEFAULT_PLATFORMS_DIR = "platforms" DEFAULT_EMULATORS_DIR = "emulators" @@ -68,173 +67,6 @@ _STATUS_ORDER = {Status.OK: 0, Status.UNTESTED: 1, Status.MISSING: 2} _SEVERITY_ORDER = {Severity.OK: 0, Severity.INFO: 1, Severity.WARNING: 2, Severity.CRITICAL: 3} -# --------------------------------------------------------------------------- -# Emulator-level validation (size, crc32 checks from emulator profiles) -# --------------------------------------------------------------------------- - -def _parse_validation(validation: list | dict | None) -> list[str]: - """Extract the validation check list from a file's validation field. - - Handles both simple list and divergent (core/upstream) dict forms. - For dicts, uses the ``core`` key since RetroArch users run the core. - """ - if validation is None: - return [] - if isinstance(validation, list): - return validation - if isinstance(validation, dict): - return validation.get("core", []) - return [] - - -# Validation types that require console-specific cryptographic keys. -# verify.py cannot reproduce these — size checks still apply if combined. -_CRYPTO_CHECKS = frozenset({"signature", "crypto"}) - -# All reproducible validation types. -_HASH_CHECKS = frozenset({"crc32", "md5", "sha1", "adler32"}) - - -def _build_validation_index(profiles: dict) -> dict[str, dict]: - """Build per-filename validation rules from emulator profiles. - - Returns {filename: {"checks": [str], "size": int|None, "min_size": int|None, - "max_size": int|None, "crc32": str|None, "md5": str|None, "sha1": str|None, - "adler32": str|None, "crypto_only": [str]}}. - - ``crypto_only`` lists validation types we cannot reproduce (signature, crypto) - so callers can report them as non-verifiable rather than silently skipping. - - When multiple emulators reference the same file, merges checks (union). - Raises ValueError if two profiles declare conflicting values. - """ - index: dict[str, dict] = {} - sources: dict[str, dict[str, str]] = {} - for emu_name, profile in profiles.items(): - if profile.get("type") in ("launcher", "alias"): - continue - for f in profile.get("files", []): - fname = f.get("name", "") - if not fname: - continue - checks = _parse_validation(f.get("validation")) - if not checks: - continue - if fname not in index: - index[fname] = { - "checks": set(), "sizes": set(), - "min_size": None, "max_size": None, - "crc32": set(), "md5": set(), "sha1": set(), "sha256": set(), - "adler32": set(), "crypto_only": set(), - } - sources[fname] = {} - index[fname]["checks"].update(checks) - # Track non-reproducible crypto checks - index[fname]["crypto_only"].update( - c for c in checks if c in _CRYPTO_CHECKS - ) - # Size checks - if "size" in checks: - if f.get("size") is not None: - index[fname]["sizes"].add(f["size"]) - if f.get("min_size") is not None: - cur = index[fname]["min_size"] - index[fname]["min_size"] = min(cur, f["min_size"]) if cur is not None else f["min_size"] - if f.get("max_size") is not None: - cur = index[fname]["max_size"] - index[fname]["max_size"] = max(cur, f["max_size"]) if cur is not None else f["max_size"] - # Hash checks — collect all accepted hashes as sets (multiple valid - # versions of the same file, e.g. MT-32 ROM versions) - if "crc32" in checks and f.get("crc32"): - norm = f["crc32"].lower() - if norm.startswith("0x"): - norm = norm[2:] - index[fname]["crc32"].add(norm) - for hash_type in ("md5", "sha1", "sha256"): - if hash_type in checks and f.get(hash_type): - index[fname][hash_type].add(f[hash_type].lower()) - # Adler32 — stored as known_hash_adler32 field (not in validation: list - # for Dolphin, but support it in both forms for future profiles) - adler_val = f.get("known_hash_adler32") or f.get("adler32") - if adler_val: - norm = adler_val.lower() - if norm.startswith("0x"): - norm = norm[2:] - index[fname]["adler32"].add(norm) - # Convert sets to sorted tuples/lists for determinism - for v in index.values(): - v["checks"] = sorted(v["checks"]) - v["crypto_only"] = sorted(v["crypto_only"]) - # Keep hash sets as frozensets for O(1) lookup in check_file_validation - return index - - -def check_file_validation( - local_path: str, filename: str, validation_index: dict[str, dict], - bios_dir: str = "bios", -) -> str | None: - """Check emulator-level validation on a resolved file. - - Supports: size (exact/min/max), crc32, md5, sha1, adler32, - signature (RSA-2048 PKCS1v15 SHA256), crypto (AES-128-CBC + SHA256). - - Returns None if all checks pass or no validation applies. - Returns a reason string if a check fails. - """ - entry = validation_index.get(filename) - if not entry: - return None - checks = entry["checks"] - - # Size checks — sizes is a set of accepted values - if "size" in checks: - actual_size = os.path.getsize(local_path) - if entry["sizes"] and actual_size not in entry["sizes"]: - expected = ",".join(str(s) for s in sorted(entry["sizes"])) - return f"size mismatch: got {actual_size}, accepted [{expected}]" - if entry["min_size"] is not None and actual_size < entry["min_size"]: - return f"size too small: min {entry['min_size']}, got {actual_size}" - if entry["max_size"] is not None and actual_size > entry["max_size"]: - return f"size too large: max {entry['max_size']}, got {actual_size}" - - # Hash checks — compute once, reuse for all hash types. - # Each hash field is a set of accepted values (multiple valid ROM versions). - need_hashes = ( - any(h in checks and entry.get(h) for h in ("crc32", "md5", "sha1", "sha256")) - or entry.get("adler32") - ) - if need_hashes: - hashes = compute_hashes(local_path) - if "crc32" in checks and entry["crc32"]: - if hashes["crc32"].lower() not in entry["crc32"]: - expected = ",".join(sorted(entry["crc32"])) - return f"crc32 mismatch: got {hashes['crc32']}, accepted [{expected}]" - if "md5" in checks and entry["md5"]: - if hashes["md5"].lower() not in entry["md5"]: - expected = ",".join(sorted(entry["md5"])) - return f"md5 mismatch: got {hashes['md5']}, accepted [{expected}]" - if "sha1" in checks and entry["sha1"]: - if hashes["sha1"].lower() not in entry["sha1"]: - expected = ",".join(sorted(entry["sha1"])) - return f"sha1 mismatch: got {hashes['sha1']}, accepted [{expected}]" - if "sha256" in checks and entry["sha256"]: - if hashes["sha256"].lower() not in entry["sha256"]: - expected = ",".join(sorted(entry["sha256"])) - return f"sha256 mismatch: got {hashes['sha256']}, accepted [{expected}]" - if entry["adler32"]: - if hashes["adler32"].lower() not in entry["adler32"]: - expected = ",".join(sorted(entry["adler32"])) - return f"adler32 mismatch: got 0x{hashes['adler32']}, accepted [{expected}]" - - # Signature/crypto checks (3DS RSA, AES) - if entry["crypto_only"]: - crypto_reason = check_crypto_validation(local_path, filename, bios_dir) - if crypto_reason: - return crypto_reason - - return None - - # --------------------------------------------------------------------------- # Verification functions # --------------------------------------------------------------------------- @@ -269,7 +101,7 @@ def verify_entry_md5( base = {"name": name, "required": required} if expected_md5 and "," in expected_md5: - md5_list = [m.strip() for m in expected_md5.split(",") if m.strip()] + md5_list = [m.strip().lower() for m in expected_md5.split(",") if m.strip()] else: md5_list = [expected_md5] if expected_md5 else [] @@ -695,19 +527,6 @@ def print_platform_result(result: dict, group: list[str]) -> None: # Emulator/system mode verification # --------------------------------------------------------------------------- -def _filter_files_by_mode(files: list[dict], standalone: bool) -> list[dict]: - """Filter file entries by libretro/standalone mode.""" - result = [] - for f in files: - fmode = f.get("mode", "") - if standalone and fmode == "libretro": - continue - if not standalone and fmode == "standalone": - continue - result.append(f) - return result - - def _effective_validation_label(details: list[dict], validation_index: dict) -> str: """Determine the bracket label for the report. @@ -783,7 +602,7 @@ def verify_emulator( data_dir_notices: list[str] = [] for emu_name, profile in selected: - files = _filter_files_by_mode(profile.get("files", []), standalone) + files = filter_files_by_mode(profile.get("files", []), standalone) # Check data directories (only notice if not cached) for dd in profile.get("data_directories", []): @@ -976,34 +795,6 @@ def print_emulator_result(result: dict) -> None: print(f" Note: data directory '{ref}' required but not included (use refresh_data_dirs.py)") -def _list_emulators(emulators_dir: str) -> None: - """Print available emulator profiles.""" - profiles = load_emulator_profiles(emulators_dir) - for name in sorted(profiles): - p = profiles[name] - if p.get("type") in ("alias", "test"): - continue - display = p.get("emulator", name) - ptype = p.get("type", "libretro") - systems = ", ".join(p.get("systems", [])[:3]) - more = "..." if len(p.get("systems", [])) > 3 else "" - print(f" {name:30s} {display:40s} [{ptype}] {systems}{more}") - - -def _list_systems(emulators_dir: str) -> None: - """Print available system IDs with emulator count.""" - profiles = load_emulator_profiles(emulators_dir) - system_emus: dict[str, list[str]] = {} - for name, p in profiles.items(): - if p.get("type") in ("alias", "test", "launcher"): - continue - for sys_id in p.get("systems", []): - system_emus.setdefault(sys_id, []).append(name) - for sys_id in sorted(system_emus): - count = len(system_emus[sys_id]) - print(f" {sys_id:35s} ({count} emulator{'s' if count > 1 else ''})") - - def main(): parser = argparse.ArgumentParser(description="Platform-native BIOS verification") parser.add_argument("--platform", "-p", help="Platform name") @@ -1021,10 +812,10 @@ def main(): args = parser.parse_args() if args.list_emulators: - _list_emulators(args.emulators_dir) + list_emulator_profiles(args.emulators_dir) return if args.list_systems: - _list_systems(args.emulators_dir) + list_system_ids(args.emulators_dir) return # Mutual exclusion diff --git a/tests/test_e2e.py b/tests/test_e2e.py index 87d503f4..fe75a979 100644 --- a/tests/test_e2e.py +++ b/tests/test_e2e.py @@ -29,14 +29,15 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "scripts")) import yaml from common import ( - build_zip_contents_index, check_inside_zip, group_identical_platforms, - load_emulator_profiles, load_platform_config, md5_composite, md5sum, - resolve_local_file, resolve_platform_cores, + _build_validation_index, build_zip_contents_index, check_file_validation, + check_inside_zip, compute_hashes, filter_files_by_mode, + group_identical_platforms, load_emulator_profiles, load_platform_config, + md5_composite, md5sum, parse_md5_list, resolve_local_file, + resolve_platform_cores, safe_extract_zip, ) from verify import ( Severity, Status, verify_platform, find_undeclared_files, find_exclusion_notes, - _build_validation_index, check_file_validation, verify_emulator, - _filter_files_by_mode, _effective_validation_label, + verify_emulator, _effective_validation_label, ) @@ -58,6 +59,10 @@ class TestE2E(unittest.TestCase): # --------------------------------------------------------------- def setUp(self): + # Clear emulator profile cache to avoid stale data between tests + from common import _emulator_profiles_cache + _emulator_profiles_cache.clear() + self.root = tempfile.mkdtemp() self.bios_dir = os.path.join(self.root, "bios") self.platforms_dir = os.path.join(self.root, "platforms") @@ -967,16 +972,16 @@ class TestE2E(unittest.TestCase): # test_validation has crc32, md5, sha1, size → all listed self.assertEqual(result["verification_mode"], "crc32+md5+sha1+signature+size") - def test_99_filter_files_by_mode(self): - """_filter_files_by_mode correctly filters standalone/libretro.""" + def test_99filter_files_by_mode(self): + """filter_files_by_mode correctly filters standalone/libretro.""" files = [ {"name": "a.bin"}, # no mode → both {"name": "b.bin", "mode": "libretro"}, # libretro only {"name": "c.bin", "mode": "standalone"}, # standalone only {"name": "d.bin", "mode": "both"}, # explicit both ] - lr = _filter_files_by_mode(files, standalone=False) - sa = _filter_files_by_mode(files, standalone=True) + lr = filter_files_by_mode(files, standalone=False) + sa = filter_files_by_mode(files, standalone=True) lr_names = {f["name"] for f in lr} sa_names = {f["name"] for f in sa} self.assertEqual(lr_names, {"a.bin", "b.bin", "d.bin"}) @@ -1012,5 +1017,86 @@ class TestE2E(unittest.TestCase): self.assertGreater(result["severity_counts"][Severity.WARNING], 0) + def test_102_safe_extract_zip_blocks_traversal(self): + """safe_extract_zip must reject zip-slip path traversal.""" + malicious_zip = os.path.join(self.root, "evil.zip") + with zipfile.ZipFile(malicious_zip, "w") as zf: + zf.writestr("../../etc/passwd", "root:x:0:0") + dest = os.path.join(self.root, "extract_dest") + os.makedirs(dest) + with self.assertRaises(ValueError): + safe_extract_zip(malicious_zip, dest) + + def test_103_safe_extract_zip_normal(self): + """safe_extract_zip extracts valid files correctly.""" + normal_zip = os.path.join(self.root, "normal.zip") + with zipfile.ZipFile(normal_zip, "w") as zf: + zf.writestr("subdir/file.txt", "hello") + dest = os.path.join(self.root, "extract_normal") + os.makedirs(dest) + safe_extract_zip(normal_zip, dest) + extracted = os.path.join(dest, "subdir", "file.txt") + self.assertTrue(os.path.exists(extracted)) + with open(extracted) as f: + self.assertEqual(f.read(), "hello") + + def test_104_compute_hashes_correctness(self): + """compute_hashes returns correct values for known content.""" + test_file = os.path.join(self.root, "hash_test.bin") + data = b"retrobios test content" + with open(test_file, "wb") as f: + f.write(data) + import zlib + expected_sha1 = hashlib.sha1(data).hexdigest() + expected_md5 = hashlib.md5(data).hexdigest() + expected_sha256 = hashlib.sha256(data).hexdigest() + expected_crc32 = format(zlib.crc32(data) & 0xFFFFFFFF, "08x") + + result = compute_hashes(test_file) + self.assertEqual(result["sha1"], expected_sha1) + self.assertEqual(result["md5"], expected_md5) + self.assertEqual(result["sha256"], expected_sha256) + self.assertEqual(result["crc32"], expected_crc32) + + def test_105_resolve_with_empty_database(self): + """resolve_local_file handles empty database gracefully.""" + empty_db = {"files": {}, "indexes": {"by_md5": {}, "by_name": {}, "by_path_suffix": {}}} + entry = {"name": "nonexistent.bin", "sha1": "abc123"} + path, status = resolve_local_file(entry, empty_db) + self.assertIsNone(path) + self.assertEqual(status, "not_found") + + def test_106_parse_md5_list(self): + """parse_md5_list normalizes comma-separated MD5s.""" + self.assertEqual(parse_md5_list(""), []) + self.assertEqual(parse_md5_list("ABC123"), ["abc123"]) + self.assertEqual(parse_md5_list("abc, DEF , ghi"), ["abc", "def", "ghi"]) + self.assertEqual(parse_md5_list(",,,"), []) + + def test_107filter_files_by_mode(self): + """filter_files_by_mode filters standalone/libretro correctly.""" + files = [ + {"name": "a.bin", "mode": "standalone"}, + {"name": "b.bin", "mode": "libretro"}, + {"name": "c.bin", "mode": "both"}, + {"name": "d.bin"}, # no mode + ] + # Libretro mode: exclude standalone + result = filter_files_by_mode(files, standalone=False) + names = [f["name"] for f in result] + self.assertNotIn("a.bin", names) + self.assertIn("b.bin", names) + self.assertIn("c.bin", names) + self.assertIn("d.bin", names) + + # Standalone mode: exclude libretro + result = filter_files_by_mode(files, standalone=True) + names = [f["name"] for f in result] + self.assertIn("a.bin", names) + self.assertNotIn("b.bin", names) + self.assertIn("c.bin", names) + self.assertIn("d.bin", names) + + if __name__ == "__main__": unittest.main()