feat: re-profile 22 emulators, refactor validation to common.py

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.
This commit is contained in:
Abdessamad Derraz
2026-03-24 22:31:22 +01:00
parent 94000bdaef
commit 0543165ed2
33 changed files with 1449 additions and 783 deletions

View File

@@ -1,30 +1,111 @@
# LRPS2 — Sony PlayStation 2 (libretro) # LRPS2 - libretro PlayStation 2 core
# ref: libretro/lrps2, docs.libretro.com/library/lrps2 # Source: https://github.com/libretro/ps2
# Same BIOS as PCSX2 standalone, placed in pcsx2/bios/ subfolder. # Upstream: https://github.com/PCSX2/pcsx2
# 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).
emulator: LRPS2 emulator: LRPS2
type: libretro 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] 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: | notes: |
Libretro port of PCSX2. Uses the same BIOS files as standalone PCSX2. Hard fork of PCSX2 ported to libretro. BIOS detection is filename-agnostic: the core
BIOS must NOT be zipped. Any valid PS2 BIOS works (SCPH-10000 not scans pcsx2/bios/ for any file between 4-8 MB with a valid romdir structure containing
recommended due to lower compatibility). BIOS goes in pcsx2/bios/. RESET and ROMVER entries. The ROMVER entry determines region and version.
GameIndex.yaml goes in pcsx2/resources/ and is needed for proper Companion files (.rom1, .rom2, .nvm, .mec) derive their paths from the selected BIOS.
game-specific settings (patches, fixes). DEV9 (network adapter) and USB are stubbed in the libretro port.
GameIndex.yaml in pcsx2/resources/ provides per-game patches for compatibility.
files: files:
- name: GameIndex.yaml - name: "<bios>.bin"
system: sony-playstation-2 path: "pcsx2/bios/"
required: true required: true
note: "game compatibility database, not a BIOS — pcsx2/resources/GameIndex.yaml" description: "PS2 BIOS binary"
source_ref: "pcsx2/GameIndex.yaml" 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: "<bios>.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 <biospath>.rom1 (appended) then <biosbase>.rom1
(extension replaced). Silently skipped if not found.
- name: "<bios>.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: "<bios>.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: "<bios>.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.

View File

@@ -1,10 +1,10 @@
emulator: Mupen64Plus-Next emulator: "Mupen64Plus-Next"
type: libretro type: libretro
core_classification: enhanced_fork core_classification: enhanced_fork
source: "https://github.com/libretro/mupen64plus-libretro-nx" source: "https://github.com/libretro/mupen64plus-libretro-nx"
upstream: "https://github.com/mupen64plus/mupen64plus-core" upstream: "https://github.com/mupen64plus/mupen64plus-core"
profiled_date: "2026-03-24" profiled_date: "2026-03-24"
core_version: "2.8" core_version: "2.6.0"
display_name: "Nintendo - Nintendo 64 (Mupen64Plus-Next)" display_name: "Nintendo - Nintendo 64 (Mupen64Plus-Next)"
systems: [nintendo-64, nintendo-64dd] systems: [nintendo-64, nintendo-64dd]
cores: [mupen64plus_next, mupen64plus_next_develop, mupen64plus_next_gles3, mupen64plus_next_gles2] cores: [mupen64plus_next, mupen64plus_next_develop, mupen64plus_next_gles3, mupen64plus_next_gles2]
@@ -14,8 +14,16 @@ files:
path: "Mupen64plus/IPL.n64" path: "Mupen64plus/IPL.n64"
size: 4194304 size: 4194304
required: false required: false
note: "64DD IPL ROM. Only needed for N64 Disk Drive games (.ndd) via subsystem API" description: "64DD IPL ROM"
source_ref: "mupen64plus-core/src/main/main.c:954-979" 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: notes:
hle_available: true hle_available: true
@@ -29,3 +37,6 @@ notes:
transferpak_note: > transferpak_note: >
Transfer Pak support (GB/GBC games on N64) handled via subsystem API. Transfer Pak support (GB/GBC games on N64) handled via subsystem API.
No additional firmware files needed. 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.

View File

@@ -1,8 +1,149 @@
emulator: "nekop2" emulator: nekop2
type: alias type: libretro
alias_of: "np2kai" core_classification: community_fork
profiled_date: "2026-03-18" 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" core_version: "0.86"
display_name: "NEC - PC-98 (Neko Project II)" display_name: "NEC - PC-98 (Neko Project II)"
note: "This core uses the same BIOS/firmware as np2kai. See emulators/np2kai.yml for details." cores: [nekop2]
files: [] 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 <system>/np2/.
# The libretro shim sets np2cfg.biospath to "<system>/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 "<system>/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"

17
emulators/nes.yml Normal file
View File

@@ -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).

View File

@@ -1,40 +1,36 @@
emulator: Nestopia UE emulator: Nestopia UE
type: libretro type: libretro
core_classification: community_fork
source: "https://github.com/libretro/nestopia" source: "https://github.com/libretro/nestopia"
upstream: "https://gitlab.com/jgemu/nestopia"
logo: "https://raw.githubusercontent.com/0ldsk00l/nestopia/master/icons/svg/nestopia.svg" 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" core_version: "1.53.1"
display_name: "Nintendo - NES / Famicom (Nestopia)" display_name: "Nintendo - NES / Famicom (Nestopia)"
cores: [nestopia]
systems: [nintendo-nes, nintendo-fds] systems: [nintendo-nes, nintendo-fds]
notes: | notes: |
Nestopia UE (Undead Edition) is a cycle-accurate NES/Famicom emulator. Nestopia UE is a cycle-accurate NES/Famicom emulator. The libretro port
NES cartridge games need no BIOS. Famicom Disk System games require tracks the upstream Nestopia JG project. NES cartridge games need no BIOS.
the FDS BIOS ROM (disksys.rom, 8 KB) loaded from the system directory. Famicom Disk System games require disksys.rom (8 KB FDS BIOS) in the
The core validates the BIOS via CRC32 against two known dumps: system directory. The core validates the BIOS via CRC32 against two known
standard Famicom (0x5E607DCF) and Twin Famicom (0x4DF24A6C). dumps: standard Famicom (0x5E607DCF) and Twin Famicom (0x4DF24A6C). An
An unknown BIOS triggers a warning but still loads. unknown BIOS triggers a warning but still loads.
NstDatabase.xml is an optional game database used for region autodetection, NstDatabase.xml is an optional game database for region autodetection,
aspect ratio selection, and 4-player adapter recognition. A copy is baked mapper selection, and 4-player adapter recognition. A copy is baked into
into the core binary (libretro/nstdatabase.hpp) and used as fallback when the core binary (libretro/nstdatabase.hpp) as fallback. The upstream
no external file is found. Placing a newer NstDatabase.xml in the system Nestopia JG requires this file; the libretro port makes it optional.
directory overrides the built-in copy.
An optional custom palette file (custom.pal, 192 bytes, 64 RGB triplets) An optional custom palette file (custom.pal, 64 RGB triplets) overrides
overrides the built-in palettes when the nestopia_palette core option is built-in palettes when the nestopia_palette core option is set to Custom.
set to Custom. The core ships with 10+ built-in palettes (Royaltea, The libretro port embeds all named palettes (Royaltea, Smooth FBx, etc.)
Smooth FBx, etc.) so the external file is rarely needed. that the upstream loads from external .pal files.
Audio samples for specific games can be placed in system/nestopia/samples/ Five Famicom games use external ADPCM audio samples placed in
as numbered .wav files (e.g., 00.wav, 01.wav). This is used for Famicom system/nestopia/samples/{game}/ as numbered .wav files. These originate
expansion audio in a few titles. from MAME sample ZIPs. Games function without them but miss some audio.
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.
files: files:
# --- Famicom Disk System BIOS (required for FDS games) --- # --- Famicom Disk System BIOS (required for FDS games) ---
@@ -43,6 +39,7 @@ files:
description: "FDS BIOS ROM" description: "FDS BIOS ROM"
required: true required: true
size: 8192 size: 8192
validation: [crc32]
md5: "ca30b50f880eb660a320674ed365ef7a" md5: "ca30b50f880eb660a320674ed365ef7a"
sha1: "57fe1bdee955bb48d357e463ccbf129496930b62" sha1: "57fe1bdee955bb48d357e463ccbf129496930b62"
source_ref: "libretro/libretro.cpp:1608-1634 (FDS load), source/core/NstFds.cpp:61-131 (Bios class, CRC32 validation)" 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" description: "Nestopia game database for region and mapper autodetection"
required: false required: false
hle_fallback: true hle_fallback: true
size: 1022369
md5: "0ee6cbdc6f5c96ce9c8aa5edb59066f4"
sha1: ~
source_ref: "libretro/libretro.cpp:1561-1586 (database load), libretro/nstdatabase.hpp (baked-in fallback)" 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) --- # --- Custom palette (optional, core option nestopia_palette = Custom) ---
- name: "custom.pal" - name: "custom.pal"
@@ -67,7 +61,41 @@ files:
required: false required: false
hle_fallback: true hle_fallback: true
size: 192 size: 192
md5: ~
sha1: ~
source_ref: "libretro/libretro.cpp:1540-1559 (palette load)" 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)"

View File

@@ -1,173 +1,204 @@
emulator: NP2kai emulator: NP2kai
type: libretro type: libretro
core_classification: enhanced_fork
source: "https://github.com/libretro/NP2kai" 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" core_version: "0.86"
display_name: "NEC - PC-98 (Neko Project II Kai)" display_name: "NEC - PC-98 (Neko Project II Kai)"
cores: [np2kai]
systems: [pc-98] systems: [pc-98]
# NP2kai is a PC-9801/9821 emulator (Neko Project II kai). # NP2kai is a PC-9801/9821 emulator by AZO234, enhanced fork of Neko Project II
# All BIOS/font/sound files are loaded from <system>/np2kai/ subdirectory. # by Yui (original upstream dead: np2.yui.ne.jp). All files load from
# The core sets np2cfg.biospath to "<system>/np2kai/" in retro_load_game() # <system>/np2kai/ subdirectory, set in retro_load_game()
# (sdl/libretro/libretro.c:1800-1815). All getbiospath() calls resolve # (sdl/libretro/libretro.c:1800-1815). All getbiospath() calls resolve
# relative to that directory. # relative to that directory.
# #
# The core has a built-in BIOS simulator (BIOS_SIMULATE) that can boot # BIOS_SIMULATE is unconditionally #define'd (bios/bios.c:78), so the core
# without a real BIOS ROM, but a real bios.rom provides better compatibility. # always has a built-in BIOS simulator. itf.rom loading (bios.c:569) is dead
# Font data is auto-generated from built-in tables if font.bmp is missing, # code behind #else of this define and is never executed.
# but Japanese kanji display requires the real font file. #
# .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 # 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) # with uppercase names (2608_BD.WAV). The built-in rhythm engine (rhythmc.c)
# uses lowercase (.wav). Both paths resolve from np2kai/. # uses lowercase (2608_bd.wav). Both resolve from np2kai/.
# The fmgen engine also accepts "2608_RYM.WAV" as fallback for the rim sample. # 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: files:
# -- Main BIOS ROM -- # -- Main BIOS ROM --
# Loaded in bios/bios.c:430-440. 96 KB (0x18000) mapped at 0xe8000. # Loaded in bios/bios.c:430-436 via getbiospath(). 96 KB (0x18000) mapped
# Without this, the built-in BIOS simulator is used (less compatible). # at 0xe8000. Only loaded when np2cfg.usebios is true (core option).
# Without this, CopyMemory copies nosyscode[] built-in simulator.
- name: "bios.rom" - name: "bios.rom"
path: "np2kai/bios.rom" path: "np2kai/bios.rom"
size: 98304
required: false required: false
hle_fallback: true hle_fallback: true
note: > note: >
PC-9801 system BIOS ROM (96 KB). Provides full hardware compatibility. PC-9801 system BIOS ROM (96 KB). The core boots without it using the
The core can boot without it using the built-in BIOS simulator, but built-in BIOS simulator, but some software requires the real BIOS.
some software may not work correctly. Loaded at address 0xe8000. source_ref: "bios/bios.c:430-436, common/strres.c:60"
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"
# -- Font file -- # -- Font file --
# Set explicitly in libretro.c:1813 as "<system>/np2kai/font.bmp". # Path set in libretro.c:1813 as "<system>/np2kai/font.bmp".
# The core also supports FONT.ROM / font.rom (V98 format) via font_load(). # font_load() in font/font.c:125 detects type by extension:
# Without any font file, built-in 8x8 bitmap data is used but kanji # .bmp/.BMP = PC98 format (fontpc98.c), FONT.ROM = V98 format (fontv98.c).
# characters will not display correctly. # Without any font file, fontdata_8 provides 8x8 ASCII only.
- name: "font.bmp" - name: "font.bmp"
path: "np2kai/font.bmp" path: "np2kai/font.bmp"
required: false required: false
hle_fallback: true hle_fallback: true
aliases: ["FONT.ROM", "font.rom", "FONT.BMP"] aliases: ["FONT.ROM", "font.rom", "FONT.BMP"]
note: > note: >
PC-98 font bitmap (288 KB). Required for correct Japanese kanji display. PC-98 font bitmap. Required for correct Japanese kanji display.
Without this file, only basic ASCII characters render correctly using Without this, only basic ASCII renders using built-in 8x8 data.
built-in data. FONT.ROM (V98 format) is also accepted.
source_ref: "sdl/libretro/libretro.c:1813, font/fontdata.c:11-14" source_ref: "sdl/libretro/libretro.c:1813, font/font.c:86-125, font/fontdata.c:11"
# -- Sound BIOS ROM -- # -- Sound BIOS ROM --
# Loaded by soundrom_load() as "sound.rom" (soundrom.c:15-16, 28-32). # Loaded by soundrom_load() in soundrom.c:93-106 via loadsoundrom().
# The filename is composed as "sound" + optional board name + ".rom". # Filename composed as "sound" + optional board name + ".rom" (soundrom.c:27-32).
# 16 KB ROM for the FM sound board. # Board-specific variants tried first, then sound.rom as fallback.
# 16 KB (0x4000). Falls back to 9-byte defsoundrom[] stub.
- name: "sound.rom" - name: "sound.rom"
path: "np2kai/sound.rom" path: "np2kai/sound.rom"
size: 16384
required: false required: false
hle_fallback: true hle_fallback: true
note: > note: >
FM sound board BIOS ROM (16 KB). Used by the PC-9801-26K/86/118 FM sound board BIOS ROM (16 KB). Generic fallback for all sound boards.
sound boards. The core falls back to a minimal built-in default Board-specific variants are tried first: sound26.rom (PC-9801-26K),
(9-byte stub) if this file is missing. 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" source_ref: "sound/soundrom.c:15-16,21-55,93-106"
# -- YM2608 OPNA rhythm samples -- # -- YM2608 OPNA rhythm samples --
# Loaded by both the built-in rhythm engine (rhythmc.c:60-71) and the # Loaded by the built-in rhythm engine (rhythmc.c:60-71) using lowercase
# fmgen engine (fmgen_opna.cpp:1413-1443). Required for YM2608 OPNA # filenames, and by the fmgen engine (fmgen_opna.cpp:1413-1443) using
# rhythm sound channel (bass drum, snare, etc). # uppercase (2608_BD.WAV etc). Both resolve via getbiospath().
# The fmgen engine tries uppercase .WAV, the built-in engine uses .wav.
# Place lowercase versions - the filesystem handles case on most platforms.
- name: "2608_bd.wav" - name: "2608_bd.wav"
path: "np2kai/2608_bd.wav" path: "np2kai/2608_bd.wav"
required: false required: false
aliases: ["2608_BD.WAV"]
note: "YM2608 OPNA rhythm sample: bass drum" 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" - name: "2608_sd.wav"
path: "np2kai/2608_sd.wav" path: "np2kai/2608_sd.wav"
required: false required: false
aliases: ["2608_SD.WAV"]
note: "YM2608 OPNA rhythm sample: snare drum" 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" - name: "2608_top.wav"
path: "np2kai/2608_top.wav" path: "np2kai/2608_top.wav"
required: false required: false
aliases: ["2608_TOP.WAV"]
note: "YM2608 OPNA rhythm sample: top cymbal" 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" - name: "2608_hh.wav"
path: "np2kai/2608_hh.wav" path: "np2kai/2608_hh.wav"
required: false required: false
aliases: ["2608_HH.WAV"]
note: "YM2608 OPNA rhythm sample: hi-hat" 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" - name: "2608_tom.wav"
path: "np2kai/2608_tom.wav" path: "np2kai/2608_tom.wav"
required: false required: false
aliases: ["2608_TOM.WAV"]
note: "YM2608 OPNA rhythm sample: tom" 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" - name: "2608_rim.wav"
path: "np2kai/2608_rim.wav" path: "np2kai/2608_rim.wav"
required: false required: false
aliases: ["2608_RYM.WAV"] aliases: ["2608_RIM.WAV", "2608_RYM.WAV"]
note: "YM2608 OPNA rhythm sample: rim shot. fmgen also accepts 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" source_ref: "sound/rhythmc.c:16, sound/fmgen/fmgen_opna.cpp:1431-1441"
# -- IDE BIOS ROM -- # -- IDE BIOS ROM --
# Loaded by ideio.c:1913-1931. Tried in order: ide.rom, d8000.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 # bank3.bin, bios9821.rom. Only loaded when np2cfg.idebios is enabled
# core options (np2cfg.idebios) and a real BIOS ROM is also present. # and a real main BIOS ROM is present. Falls back to simulated IDE BIOS.
- name: "ide.rom" - name: "ide.rom"
path: "np2kai/ide.rom" path: "np2kai/ide.rom"
size: 8192
required: false required: false
hle_fallback: true hle_fallback: true
aliases: ["d8000.rom", "bank3.bin", "bios9821.rom"] aliases: ["d8000.rom", "bank3.bin", "bios9821.rom"]
note: > note: >
IDE controller BIOS ROM (8 KB). Required for real IDE BIOS emulation IDE controller BIOS ROM (8 KB). For IDE HDD boot support.
(HDD boot from IDE). Without this, a simulated IDE BIOS is used. Without this, a simulated IDE BIOS is used.
source_ref: "cbus/ideio.c:1913-1931" source_ref: "cbus/ideio.c:1913-1941"
# -- SCSI BIOS ROM -- # -- 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" - name: "scsi.rom"
path: "np2kai/scsi.rom" path: "np2kai/scsi.rom"
size: 16384
required: false required: false
hle_fallback: true hle_fallback: true
note: > note: >
SCSI controller BIOS ROM (16 KB). For PC-98 SCSI HDD support. SCSI controller BIOS ROM (16 KB). The core includes a built-in
The core includes a built-in SCSI BIOS stub as fallback. SCSI BIOS stub as fallback.
source_ref: "cbus/scsiio.c:219-231" 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 -- # -- PCI BIOS ROM --
# Loaded by pcidev.c:364-382. Tries pci.rom then bank0.bin. # Loaded by pcidev.c:360-382. Tries pci.rom then bank0.bin.
# Falls back to built-in PCI BIOS simulation. # 32 KB (0x8000). Falls back to built-in PCI BIOS simulation.
- name: "pci.rom" - name: "pci.rom"
path: "np2kai/pci.rom" path: "np2kai/pci.rom"
size: 32768
required: false required: false
hle_fallback: true hle_fallback: true
aliases: ["bank0.bin"] aliases: ["bank0.bin"]
note: > note: >
PCI BIOS ROM (32 KB). For PC-9821 PCI bus emulation. PCI BIOS ROM (32 KB). For PC-9821 PCI bus emulation.
Without this, the built-in PCI BIOS simulator is used. 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 -- # -- 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" - name: "gpib.rom"
path: "np2kai/gpib.rom" path: "np2kai/gpib.rom"
size: 8192
required: false required: false
note: > note: >
GP-IB interface BIOS ROM. Rarely needed, only for GP-IB peripheral GP-IB interface BIOS ROM (8 KB). If missing, GP-IB emulation
emulation. is disabled entirely.
source_ref: "cbus/gpibio.c:327-356" 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"

View File

@@ -1,7 +1,9 @@
emulator: Numero emulator: Numero
type: libretro type: libretro
core_classification: community_fork
source: "https://github.com/nbarkhina/numero" 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" core_version: "v1.0"
display_name: "Texas Instruments TI-83 (Numero)" display_name: "Texas Instruments TI-83 (Numero)"
cores: cores:
@@ -10,38 +12,35 @@ systems:
- ti-83 - ti-83
notes: | notes: |
Numero is a TI-83 family calculator emulator for libretro, based on the Libretro port of Wabbitemu, a TI-83 family calculator emulator.
Wabbitemu emulator. Supports TI-83, TI-83 Plus and TI-83 Silver Edition. 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 A calculator ROM dump is required. The core searches sequentially in the
in the system directory root with a fixed priority order system directory (libretronew.cpp:575-594): ti83se.rom first, then
(libretronew.cpp:575-594): 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) Without any ROM, the core shows a blank screen listing the expected
2. ti83plus.rom (TI-83 Plus) filenames (libretronew.cpp:1030-1042). No HLE fallback.
3. ti83.rom (TI-83)
The first file found wins. file_present_in_system() checks existence only Supports no-content launch (libretronew.cpp:556). Content files
(libretronew.cpp:451-472), then rom_load() loads it (libretronew.cpp:608). (.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, Upstream Wabbitemu supports a wider model range (TI-73 through TI-84+CSE)
the core starts without a BIOS and shows a blank screen with a status but the libretro wrapper only searches for TI-83 family filenames. The
message listing the expected filenames (libretronew.cpp:1034-1041). underlying engine (calc.cpp:rom_load) auto-detects the model from the ROM
header.
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).
files: files:
- name: "ti83se.rom" - name: "ti83se.rom"
system: ti-83 system: ti-83
description: "TI-83 Silver Edition ROM dump" description: "TI-83 Plus Silver Edition ROM dump"
required: false required: false
source_ref: "libretronew.cpp:576-578" 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" - name: "ti83plus.rom"
system: ti-83 system: ti-83
@@ -55,28 +54,4 @@ files:
description: "TI-83 ROM dump" description: "TI-83 ROM dump"
required: false required: false
source_ref: "libretronew.cpp:591-593" source_ref: "libretronew.cpp:591-593"
notes: "Checked last. Original TI-83 (non-Plus) ROM." notes: "Checked last. Original TI-83 (non-Plus)."
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.

View File

@@ -1,45 +1,38 @@
emulator: NXEngine emulator: NXEngine
type: libretro type: libretro
core_classification: game_engine
source: "https://github.com/libretro/nxengine-libretro" 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" core_version: "1.0.0.6"
display_name: "Cave Story (NXEngine)" display_name: "Cave Story (NXEngine)"
cores: [nxengine]
systems: [cave-story] systems: [cave-story]
notes: | notes: |
NXEngine is an open-source reimplementation of the Cave Story (Doukutsu 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 Monogatari) engine by Studio Pixel, authored by Caitlin Shaw. It is not
that loads the original freeware game data directly. an emulator but a source port that loads the original freeware game data.
The core requires the original freeware Cave Story distribution placed in The core requires the freeware Cave Story distribution placed in
{system_dir}/nxengine/. When launched without a content file, it looks for {system_dir}/nxengine/. When launched without content, it looks for
Doukutsu.exe at that path (libretro.cpp:254-259). When launched with a .exe Doukutsu.exe there (libretro.cpp:254-259). When launched with a .exe
content file, it uses the parent directory of that file instead content file, it uses the parent directory of that file (libretro.cpp:237).
(libretro.cpp:244).
At startup (main.cpp:66-90), the core opens Doukutsu.exe and extracts from At startup (main.cpp:75-90), the core opens Doukutsu.exe and extracts:
it: ORG music files (extractorg.c), PXT sound effects (extractpxt.c), stage ORG music (extractorg.c), PXT sound effects (extractpxt.c), stage tile
tile attributes (extractstages.c), bitmap graphics, and the wavetable attributes (extractstages.c), 18 credit BMPs + pixel.bmp (endpic/),
(cachefiles.c:484). These are kept in memory, not written to disk. and the wavetable (cachefiles.c:464-485). All kept in memory.
The data/ directory must exist alongside Doukutsu.exe with the full game The data/ directory (399 files from the freeware release) is loaded into
asset tree: 30 root-level files (sprites, backgrounds, script tables), an in-memory cache at init (cachefiles.c:38-439, 497-540). All subsequent
36 NPC sprite sheets in data/Npc/, and 333 stage files in data/Stage/ file access goes through this cache. The core verifies data/ by checking
(maps .pxm, tile attributes .pxa, entity lists .pxe, scripts .tsc, and for data/npc.tbl (main.cpp:47-58).
tileset images via Prt*.pbm). The core verifies data/ exists by checking
for data/npc.tbl (main.cpp:47-63).
The core also reads endpic/ bitmaps for ending sequences, extracted from The font is compiled-in (bitmap_font.h). The upstream standalone version
Doukutsu.exe at runtime (cachefiles.c:460-484). uses font.ttf via SDL_ttf.
All required files ship in the datafiles/ directory of the libretro repo Valid content extension: .exe (libretro.cpp:109).
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).
files: files:
- name: "Doukutsu.exe" - name: "Doukutsu.exe"
@@ -51,20 +44,37 @@ files:
md5: "38695d3d69d7a0ada8178072dad4c58b" md5: "38695d3d69d7a0ada8178072dad4c58b"
sha1: "bb2d0441e073da9c584f23c2ad8c7ab8aac293bf" sha1: "bb2d0441e073da9c584f23c2ad8c7ab8aac293bf"
source_ref: "main.cpp:77-78 (opened for extraction), libretro.cpp:258 (existence check)" 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." 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/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."
- name: "data/" - name: "data/"
category: game_data category: game_data
system: cave-story system: cave-story
description: "Full game asset directory tree (399 files: sprites, NPC sheets, stage maps, scripts, backgrounds)" description: "Full game asset directory tree (399 files: sprites, NPC sheets, stage maps, scripts, backgrounds)"
required: true 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." 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."

View File

@@ -1,8 +1,11 @@
emulator: O2EM emulator: O2EM
type: libretro type: libretro
core_classification: community_fork
core: o2em_libretro core: o2em_libretro
cores: [o2em_libretro]
source: "https://github.com/libretro/libretro-o2em" 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" core_version: "1.18"
display_name: "Magnavox - Odyssey2 / Philips Videopac+ (O2EM)" display_name: "Magnavox - Odyssey2 / Philips Videopac+ (O2EM)"
systems: systems:
@@ -10,28 +13,24 @@ systems:
- videopac - videopac
notes: | notes: |
O2EM is the libretro port of the Odyssey2/Videopac emulator. Libretro port of O2EM, an Odyssey2/Videopac emulator by Daniel Boris
The core requires exactly one BIOS ROM selected via the o2em_bios core option. and André de la Rocha. Port by Arlindo M. de Oliveira.
Default is o2rom.bin (Odyssey2 NTSC). The user can switch to c52.bin, g7400.bin
or jopac.bin for European/French Videopac variants.
BIOS files are loaded from the system directory into rom_table[0] (1024 bytes). One BIOS ROM selected via o2em_bios core option (default o2rom.bin).
The core identifies the BIOS variant by CRC32 and sets the vpp flag accordingly Loaded into rom_table[0] (1024 bytes), identified by CRC32.
(vpp=1 enables Videopac+ enhanced graphics for g7400.bin and jopac.bin). 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. Voice module ("The Voice" speech synthesis add-on) emulation via WAV
Any single BIOS from the list below is sufficient for its region/hardware. 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 loading: libretro.c load_bios() lines 145-212.
BIOS selection: libretro_core_options.h lines 52-64.
BIOS detection: libretro.c load_bios() lines 146-212, CRC32 switch. Voice loading: voice.c init_voice() lines 38-79, called from libretro.c:1025-1032.
BIOS selection: libretro_core_options.h o2em_bios option lines 52-64.
Core option handling: libretro.c lines 1107-1134.
files: files:
# -------------------------------------------------------
# Magnavox Odyssey2 (NTSC) - G7000 model
# -------------------------------------------------------
- name: "o2rom.bin" - name: "o2rom.bin"
system: odyssey2 system: odyssey2
region: [north-america] region: [north-america]
@@ -41,12 +40,9 @@ files:
sha1: "b2e1955d957a475de2411770452eff4ea19f4cee" sha1: "b2e1955d957a475de2411770452eff4ea19f4cee"
crc32: "8016a315" crc32: "8016a315"
validation: [size, crc32] 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" source_ref: "libretro.c:182-186"
# -------------------------------------------------------
# Philips Videopac G7000 (European)
# -------------------------------------------------------
- name: "c52.bin" - name: "c52.bin"
system: videopac system: videopac
region: [europe] region: [europe]
@@ -56,12 +52,9 @@ files:
sha1: "a6120aed50831c9c0d95dbdf707820f601d9452e" sha1: "a6120aed50831c9c0d95dbdf707820f601d9452e"
crc32: "a318e8d6" crc32: "a318e8d6"
validation: [size, crc32] 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" source_ref: "libretro.c:192-197"
# -------------------------------------------------------
# Philips Videopac+ G7400 (European)
# -------------------------------------------------------
- name: "g7400.bin" - name: "g7400.bin"
system: videopac system: videopac
region: [europe] region: [europe]
@@ -71,12 +64,9 @@ files:
sha1: "5130243429b40b01a14e1304d0394b8459a6fbae" sha1: "5130243429b40b01a14e1304d0394b8459a6fbae"
crc32: "e20a9f41" crc32: "e20a9f41"
validation: [size, crc32] 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" source_ref: "libretro.c:187-191"
# -------------------------------------------------------
# Philips Videopac+ G7400 (French) - JoPac
# -------------------------------------------------------
- name: "jopac.bin" - name: "jopac.bin"
system: videopac system: videopac
region: [france] region: [france]
@@ -86,20 +76,11 @@ files:
sha1: "54b8d2c1317628de51a85fc1c424423a986775e4" sha1: "54b8d2c1317628de51a85fc1c424423a986775e4"
crc32: "11647ca5" crc32: "11647ca5"
validation: [size, crc32] 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" source_ref: "libretro.c:198-203"
platform_details: - name: "voice/"
odyssey2: required: false
bios_size: 1024 category: game_data
bios_selection: "core option o2em_bios, default o2rom.bin" note: "The Voice speech synthesis WAV samples. 9 banks (E4, E8-EF) × 128 samples."
detection_method: "CRC32 of 1024-byte ROM" source_ref: "voice.c:38-79, libretro.c:1025-1032"
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"

View File

@@ -1,9 +1,10 @@
emulator: Oberon emulator: Oberon
type: libretro type: libretro
core_classification: community_fork
source: "https://github.com/libretro/oberon-risc-emu" 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" core_version: "2020-07-01"
display_name: "Project Oberon RISC"
display_name: "Oberon RISC Emulator" display_name: "Oberon RISC Emulator"
cores: cores:
- oberon - oberon
@@ -11,33 +12,15 @@ systems: [oberon]
# Project Oberon RISC emulator by Peter De Wachter. # Project Oberon RISC emulator by Peter De Wachter.
# Emulates the Oberon RISC processor designed by Niklaus Wirth. # Emulates the Oberon RISC processor designed by Niklaus Wirth.
# The bootloader (512 words) is compiled into the binary from risc-boot.inc, # Bootloader (512 words) compiled into the binary from risc-boot.inc,
# loaded into ROM at 0xFFFFF800 on startup (risc.c:75-77, risc_new). # 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 containing the full Oberon operating system. # Content: .dsk disk images loaded via retro_load_game (libretro.c:209-214).
# The disk image is loaded via retro_load_game(game->path) and attached # Upstream and libretro core emulation code (risc.c, disk.c) are identical.
# 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.
files: [] files: []
notes: 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: > boot_process: >
CPU starts execution at ROM address 0xFFFFF800. The embedded bootloader Bootloader embedded in ROM reads boot sector from SPI disk, loads
reads the boot sector from the SPI disk and loads the Oberon inner core Oberon inner core into RAM. Content is raw sector .dsk images.
(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).

View File

@@ -1,7 +1,9 @@
emulator: ONScripter emulator: ONScripter
type: libretro type: libretro
core_classification: community_fork
source: "https://github.com/iyzsong/onscripter-libretro" 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" core_version: "0.3"
display_name: "ONScripter" display_name: "ONScripter"
cores: cores:
@@ -10,30 +12,18 @@ systems:
- onscripter - onscripter
notes: | notes: |
ONScripter is a clone of NScripter, a Japanese visual novel engine by Clone of NScripter, a Japanese visual novel engine. The libretro port
Naoki Takahashi. The libretro port by iyzsong wraps the upstream engine wraps upstream ogapee/onscripter (tag 20230825) via an SDL-to-libretro
(ogapee/onscripter, tag 20230825) with an SDL-to-libretro shim layer. shim.
No BIOS or system files are required. The .info file declares no No system files required. The .info declares no firmware. The core
firmware entries and the core never calls never calls RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY. All files
RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY. (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 default.ttf, registry.txt, dll.txt are per-game assets shipped with
script file. The core opens txt, dat, ___, or ons script files via each visual novel, not system-level files.
ONScripter::openScript() and reads all game assets (graphics, audio,
fonts) from the same directory.
The engine looks for a font file named default.ttf in the game Experimental core. No save state support (retro_serialize_size = 0).
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.
files: [] files: []

View File

@@ -1,7 +1,9 @@
emulator: ONScripter Yuri emulator: ONScripter Yuri
type: game type: game
source: "https://github.com/libretro/libretro-onsyuri" core_classification: game_engine
profiled_date: "2026-03-18" source: "https://github.com/YuriSizuku/OnscripterYuri"
upstream: "https://github.com/YuriSizuku/OnscripterYuri"
profiled_date: "2026-03-24"
core_version: "0.7.4+2" core_version: "0.7.4+2"
display_name: "ONScripter Yuri" display_name: "ONScripter Yuri"
cores: cores:
@@ -10,13 +12,10 @@ systems:
- onscripter - onscripter
notes: | notes: |
ONScripter Yuri is a fork of ONScripter optimized for the libretro Clone of the NScripter visual novel engine, forked from ONScripter-Jh.
environment. Like the original ONScripter, it is a clone of the The libretro port lives in the upstream repo (src/onsyuri_libretro/).
NScripter visual novel engine and runs .txt, .dat, and .ons script No system files required. Game scripts (0.txt, nscript.dat, etc.),
files. fonts (default.ttf), and assets are loaded from the game directory.
The core does not use RetroArch's system directory.
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.
files: [] files: []

View File

@@ -1,7 +1,9 @@
emulator: OpenLara emulator: OpenLara
type: libretro type: libretro
core_classification: game_engine
source: "https://github.com/libretro/OpenLara" 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" core_version: "v1"
display_name: "Tomb Raider (OpenLara)" display_name: "Tomb Raider (OpenLara)"
cores: [openlara] cores: [openlara]
@@ -9,23 +11,14 @@ systems: [tomb-raider]
verification: existence verification: existence
notes: > notes: >
Open-source reimplementation of the classic Tomb Raider engine (TR1-TR4). Open-source reimplementation of the classic Tomb Raider engine (TR1-TR4).
Supports PC (.PHD, .TR2, .TR4), PlayStation (.PSX), and Saturn (.SAT) Accepts phd|psx|tr2|sat extensions (main.cpp:246). need_fullpath is true -
level formats. The core accepts phd|psx|tr2|sat extensions the core resolves game assets relative to the content directory.
(retro_get_system_info sets valid_extensions, main.cpp:246). retro_load_game (main.cpp:602) derives contentDir from the level file path
need_fullpath is true - the core resolves sibling game assets relative and strips level/ if present. Game version auto-detected by probing for
to the content directory. retro_load_game (main.cpp:602) derives known level files (gameflow.h:1047-1071). Audio tracks loaded from audio/
contentDir from the loaded level file path and strips the level/ subdirectories as .ogg/.mp3/.wav files or cdaudio.wad (gameflow.h:1272-1356).
subdirectory if present, then passes a relative levelpath to the engine. System directory used only for shader cache at {system_dir}/openlara/cache/
Game version is auto-detected by probing for known level files: (main.cpp:177-197). No BIOS, firmware, or engine data files required in
DATA/GYM.PHD (TR1 PC), PSXDATA/GYM.PSX (TR1 PSX), DATA/GYM.SAT system directory. .info omits sat from supported_extensions (code declares it).
(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.
files: [] files: []

View File

@@ -1,7 +1,9 @@
emulator: OpenTyrian emulator: OpenTyrian
type: libretro type: libretro
core_classification: game_engine
source: "https://github.com/trapexit/libretro-opentyrian" 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" core_version: "1.0.0.6"
display_name: "Tyrian (OpenTyrian)" display_name: "Tyrian (OpenTyrian)"
cores: [opentyrian] cores: [opentyrian]

View File

@@ -1,8 +1,11 @@
emulator: Opera (4DO) emulator: Opera (4DO)
type: libretro type: libretro
core_classification: community_fork
core: opera_libretro core: opera_libretro
cores: [opera]
source: "https://github.com/libretro/opera-libretro" 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" core_version: "1.0.0"
display_name: "The 3DO Company - 3DO (Opera)" display_name: "The 3DO Company - 3DO (Opera)"
systems: systems:
@@ -27,7 +30,8 @@ notes: |
BIOS definitions: libopera/opera_bios.c BIOSES[] lines 3-136. BIOS definitions: libopera/opera_bios.c BIOSES[] lines 3-136.
BIOS loading: opera_lr_opts.c opera_lr_opts_set_bios() lines 239-270. 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. Core option: libretro_core_options.c opera_bios / opera_font.
files: files:

View File

@@ -1,36 +1,36 @@
emulator: Panda3DS emulator: Panda3DS
type: libretro type: standalone + libretro
core_classification: official_port
core_name: panda3ds_libretro core_name: panda3ds_libretro
source: "https://github.com/panda3ds-emu/panda3ds" 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" core_version: "Git"
display_name: "Nintendo - 3DS (Panda3DS)" display_name: "Nintendo - 3DS (Panda3DS)"
cores: [panda3ds]
systems: [nintendo-3ds] systems: [nintendo-3ds]
notes: | 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 Encrypted ROMs need AES keys in sysdata/aes_keys.txt, and seed-encrypted
titles (9.6+) also need sysdata/seeddb.bin. titles (9.6+) also need sysdata/seeddb.bin.
DSP firmware is loaded from the game itself (not from disk), with HLE/LLE/Null DSP firmware is loaded from the game itself (HLE/LLE/Null modes via core
modes selectable via core option panda3ds_dsp_emulation. option panda3ds_dsp_emulation). System archives (shared font, bad word
System archives (shared font, bad word list, country list, mii data) are list, country list, mii data) are compiled into the binary via cmrc.
compiled into the binary from citra_system_archives headers. The libretro core uses the save directory, not RetroArch's system
The libretro core does NOT use RetroArch's system directory. It stores data directory. Files go under <save_dir>/Emulator Files/sysdata/.
under the save directory in "Emulator Files/sysdata/". The .info declares firmware_count=0. is_experimental = true.
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.
files: files:
- name: "aes_keys.txt" - name: "aes_keys.txt"
path: "Emulator Files/sysdata/aes_keys.txt" path: "Emulator Files/sysdata/aes_keys.txt"
description: "AES encryption keys for decrypting encrypted ROMs" description: "AES encryption keys for decrypting encrypted ROMs"
required: false 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: | notes: |
Loaded at ROM load time from appDataRoot/sysdata/aes_keys.txt. Loaded at ROM load time from appDataRoot/sysdata/aes_keys.txt.
In libretro mode, appDataRoot = <save_dir>/Emulator Files/. In libretro mode, appDataRoot = <save_dir>/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. Only needed for encrypted .3ds/.cci/.cxi/.app files.
Decrypted dumps work without this file. Decrypted dumps work without this file.
@@ -38,8 +38,8 @@ files:
path: "Emulator Files/sysdata/seeddb.bin" path: "Emulator Files/sysdata/seeddb.bin"
description: "Seed database for seed-encrypted games" description: "Seed database for seed-encrypted games"
required: false 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: | notes: |
Required for titles using seed encryption (firmware 9.6+). Used for titles with seed encryption (firmware 9.6+).
Must be placed alongside aes_keys.txt in the sysdata directory. Placed alongside aes_keys.txt in the sysdata directory.
Without it, seed-encrypted titles will fail to load with a warning. Without it, seed-encrypted titles fail to load with a warning.

View File

@@ -1,16 +1,33 @@
emulator: "ParaLLEl N64" emulator: "ParaLLEl N64"
type: libretro type: libretro
core_classification: community_fork
source: "https://github.com/libretro/parallel-n64" source: "https://github.com/libretro/parallel-n64"
upstream: "https://github.com/mupen64plus/mupen64plus-core"
profiled_date: "2026-03-24" profiled_date: "2026-03-24"
core_version: "2.0-rc2" core_version: "2.0-rc2"
display_name: "Nintendo - Nintendo 64 (ParaLLEl N64)" display_name: "Nintendo - Nintendo 64 (ParaLLEl N64)"
systems: [nintendo-64, nintendo-64dd] systems: [nintendo-64, nintendo-64dd]
cores: [parallel_n64, parallel_n64_debug] cores: [parallel_n64, parallel_n64_debug]
# Needs full source-verified profiling. Minimal profile based on known file paths.
files: files:
- name: "64DD_IPL.bin" - name: "64DD_IPL.bin"
path: "64DD_IPL.bin" path: "64DD_IPL.bin"
required: false 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.

View File

@@ -1,8 +1,8 @@
emulator: "parallel_n64_debug" emulator: "parallel_n64_debug"
type: alias type: alias
alias_of: "mupen64plus" alias_of: "parallel_n64"
profiled_date: "2026-03-18" profiled_date: "2026-03-24"
core_version: "2.0-rc2" core_version: "2.0-rc2"
display_name: "Nintendo - Nintendo 64 (ParaLLEl) (Debug)" 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: [] files: []

View File

@@ -1,13 +1,14 @@
emulator: Pascal Pong emulator: Pascal Pong
type: game type: game
source: "https://github.com/libretro/libretro-pong" core_classification: pure_libretro
profiled_date: "2026-03-18" 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" core_version: "v1.0"
display_name: "PascalPong" display_name: "PascalPong"
cores: [pascal_pong] cores: [pascal_pong]
systems: [] systems: []
files: [] files: []
notes: > notes: >
Pong clone written in Pascal as a libretro core demonstration. Pong clone written in Pascal for libretro. Self-contained,
Self-contained with all rendering done programmatically. all assets embedded in the binary. No external files loaded.
No content file, BIOS, or system directory files required.

View File

@@ -1,17 +1,45 @@
emulator: PCem emulator: PCem
type: libretro type: libretro
core_classification: community_fork
source: "https://github.com/libretro/libretro-pcem" source: "https://github.com/libretro/libretro-pcem"
profiled_date: "2026-03-18" upstream: "https://github.com/sarah-walker-pcem/pcem"
core_version: "SVN" profiled_date: "2026-03-24"
core_version: "v11 (PCem-mooch, based on PCem v10.1)"
display_name: "PC (PCem)" 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. notes:
# All ROMs are loaded relative to the core's system directory via romfopen() rom_structure: >
# which prepends pcempath (= RetroArch system dir) to the path. All ROM files are loaded relative to the RetroArch system directory via
# Each machine model requires its own BIOS ROM set under roms/<machine>/. romfopen(). Machine BIOS ROMs go in roms/<machine_name>/ subdirectories.
# Video card ROMs are separate and only needed for the selected GPU. Video card ROMs go directly in roms/ (or roms/<card>/ for some).
# The font ROM (mda.rom) is always loaded at startup. 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: files:
# ======================================================== # ========================================================
@@ -150,14 +178,18 @@ files:
- name: "Tandy 1000 SL/2 BIOS low" - name: "Tandy 1000 SL/2 BIOS low"
path: "roms/tandy1000sl2/8079047.hu1" path: "roms/tandy1000sl2/8079047.hu1"
required: false required: false
note: "Tandy 1000 SL/2 BIOS low chip. Paired with 8079048.hu2." note: >
source_ref: "src/mem.c:180" 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" - name: "Tandy 1000 SL/2 BIOS high"
path: "roms/tandy1000sl2/8079048.hu2" path: "roms/tandy1000sl2/8079048.hu2"
required: false required: false
note: "Tandy 1000 SL/2 BIOS high chip. Paired with 8079047.hu1." note: >
source_ref: "src/mem.c:181" 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 # MACHINE BIOS ROMs - Amstrad / Sinclair
@@ -360,7 +392,7 @@ files:
- name: "Acer 386SX BIOS" - name: "Acer 386SX BIOS"
path: "roms/acer386/acer386.bin" path: "roms/acer386/acer386.bin"
required: false 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" source_ref: "src/mem.c:353-356"
- name: "Acer 386SX OTI-067 video BIOS" - name: "Acer 386SX OTI-067 video BIOS"
@@ -634,8 +666,8 @@ files:
path: "roms/mach64gx/bios.bin" path: "roms/mach64gx/bios.bin"
required: false required: false
note: > note: >
ATI Graphics Pro Turbo (Mach64 GX) video BIOS. Also used by the ATI Graphics Pro Turbo (Mach64 GX) video BIOS. Also loaded by the
Cirrus Logic CL-GD5429 init path (likely a copy/paste in the source). Cirrus Logic init path (copy/paste in source).
source_ref: "src/vid_ati_mach64.c:2341, src/vid_cirrus.c:2324" source_ref: "src/vid_ati_mach64.c:2341, src/vid_cirrus.c:2324"
- name: "Cirrus Logic CL-GD5429 BIOS" - name: "Cirrus Logic CL-GD5429 BIOS"
@@ -696,16 +728,224 @@ files:
note: "NE2000 ISA Ethernet boot ROM (64 KB)." note: "NE2000 ISA Ethernet boot ROM (64 KB)."
source_ref: "src/ne2000.c:1701" source_ref: "src/ne2000.c:1701"
notes: # ========================================================
rom_structure: > # NVR/CMOS TEMPLATES (shipped with PCem distribution)
All ROM files are loaded relative to the RetroArch system directory. # Pre-configured BIOS settings. The emulator loads these on
Machine BIOS ROMs go in roms/<machine_name>/ subdirectories. # startup and writes back on shutdown. Without them the BIOS
Video card ROMs go directly in roms/ (or roms/<card>/ for some). # starts with blank CMOS settings.
Which ROMs are needed depends on the selected machine model and video card. # ========================================================
minimum_requirement: > - name: "NVR: Amstrad PC1512"
At minimum, the mda.rom font and one machine BIOS set are required. path: "pc1512.nvr"
The video card ROM for the selected GPU is also needed unless the machine required: false
has built-in video (PCjr, Tandy, Amstrad, Acer 386SX, PS/1). note: "CMOS/NVRAM template for Amstrad PC1512."
libretro_version: > source_ref: "src/nvr.c:390"
Based on PCem v10.1. The libretro port is quite old and may not match
current standalone PCem/86Box ROM paths. - 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"

View File

@@ -1,8 +1,46 @@
emulator: "pcsx1" emulator: PCSX1
type: alias type: libretro
alias_of: "pcsx_rearmed" core_classification: community_fork
profiled_date: "2026-03-18" source: "https://github.com/libretro/pcsx1-libretro"
core_version: "r21" upstream: "https://github.com/notaz/pcsx_rearmed"
profiled_date: "2026-03-24"
core_version: "r22"
display_name: "Sony - PlayStation (PCSX1)" display_name: "Sony - PlayStation (PCSX1)"
note: "This core uses the same BIOS/firmware as pcsx_rearmed. See emulators/pcsx_rearmed.yml for details." cores: [pcsx1]
files: [] 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.

View File

@@ -1,13 +1,13 @@
emulator: PCSX-ReARMed emulator: PCSX-ReARMed
type: libretro type: libretro
core_classification: embedded_hle
source: "https://github.com/libretro/pcsx_rearmed" source: "https://github.com/libretro/pcsx_rearmed"
profiled_date: "2026-03-18" upstream: "https://github.com/notaz/pcsx_rearmed"
core_version: "r24" profiled_date: "2026-03-24"
core_version: "r25"
display_name: "Sony - PlayStation (PCSX ReARMed)" display_name: "Sony - PlayStation (PCSX ReARMed)"
cores: [pcsx_rearmed, pcsx_rearmed_neon, pcsx_rearmed_interpreter] cores: [pcsx_rearmed, pcsx_rearmed_neon, pcsx_rearmed_interpreter]
systems: [sony-playstation] systems: [sony-playstation]
bios_size: 524288 # 512 KB (0x80000); also accepts 4 MB psxonpsp combo (reads first 512 KB)
verification: crc32
notes: > notes: >
HLE BIOS built-in: core option pcsx_rearmed_bios = "HLE" bypasses real BIOS entirely. 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. 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. 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. 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[]. 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: files:
# -- Region: Japan (PSX_REGION_JP = 1) -- # -- Region: Japan (PSX_REGION_JP = 1) --
@@ -25,10 +24,11 @@ files:
- name: "scph5500.bin" - name: "scph5500.bin"
description: "SCPH-5500 (v3.0 09-09-96 J)" description: "SCPH-5500 (v3.0 09-09-96 J)"
region: "NTSC-J" region: "NTSC-J"
size: 524288
required: false required: false
hle_fallback: true hle_fallback: true
md5: "8dd7d5296a650fac7319bce665a6a53c" validation: [size]
source_ref: "frontend/libretro.c:3710 (listed_bios[0])" source_ref: "frontend/libretro.c:3721-3724 (listed_bios[0])"
notes: "Preferred JP BIOS, searched first by exact filename." notes: "Preferred JP BIOS, searched first by exact filename."
# -- Region: North America (PSX_REGION_US = 0) -- # -- Region: North America (PSX_REGION_US = 0) --
@@ -36,10 +36,11 @@ files:
- name: "scph5501.bin" - name: "scph5501.bin"
description: "SCPH-5501 (v3.0 11-18-96 A)" description: "SCPH-5501 (v3.0 11-18-96 A)"
region: "NTSC-U" region: "NTSC-U"
size: 524288
required: false required: false
hle_fallback: true hle_fallback: true
md5: "490f666e1afb15b7362b406ed1cea246" validation: [size]
source_ref: "frontend/libretro.c:3710 (listed_bios[1])" source_ref: "frontend/libretro.c:3721-3724 (listed_bios[1])"
notes: "Preferred US BIOS, searched first by exact filename." notes: "Preferred US BIOS, searched first by exact filename."
# -- Region: Europe (PSX_REGION_EU = 2) -- # -- Region: Europe (PSX_REGION_EU = 2) --
@@ -47,48 +48,52 @@ files:
- name: "scph5502.bin" - name: "scph5502.bin"
description: "SCPH-5502 (v3.0 01-06-97 E)" description: "SCPH-5502 (v3.0 01-06-97 E)"
region: "PAL" region: "PAL"
size: 524288
required: false required: false
hle_fallback: true hle_fallback: true
md5: "32736f17079d0b2b7024407c39bd3050" validation: [size]
source_ref: "frontend/libretro.c:3710 (listed_bios[2])" source_ref: "frontend/libretro.c:3721-3724 (listed_bios[2])"
notes: "Preferred EU BIOS, searched first by exact filename." notes: "Preferred EU BIOS, searched first by exact filename."
# -- Fallback BIOS filenames (searched in order after scph550x) -- # -- Fallback BIOS filenames (searched in order after scph550x) --
- name: "psxonpsp660.bin" - 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" region: "Auto"
size: 524288
required: false required: false
hle_fallback: true hle_fallback: true
md5: "c53ca5908936d412331790f4426c6c33" validation: [size]
source_ref: "frontend/libretro.c:3711 (listed_bios[3])" source_ref: "frontend/libretro.c:3721-3724 (listed_bios[3])"
notes: "Region detected from content at runtime. 4 MB combo image accepted but only first 512 KB read." notes: "Region detected from content at runtime. Must be 512 KB in libretro context."
- name: "scph101.bin" - name: "scph101.bin"
description: "SCPH-101 (v4.4 03-24-00 A) - PSone US" description: "SCPH-101 (v4.4 03-24-00 A) - PSone US"
region: "NTSC-U" region: "NTSC-U"
size: 524288
required: false required: false
hle_fallback: true hle_fallback: true
md5: "6e3735ff4c7dc899ee98981c18c3666d" validation: [size]
source_ref: "frontend/libretro.c:3711 (listed_bios[4])" source_ref: "frontend/libretro.c:3721-3724 (listed_bios[4])"
notes: "PSone slim model. Searched by filename after scph550x and psxonpsp660." notes: "PSone slim model."
- name: "scph7001.bin" - name: "scph7001.bin"
description: "SCPH-7001 (v4.1 12-16-97 A)" description: "SCPH-7001 (v4.1 12-16-97 A)"
region: "NTSC-U" region: "NTSC-U"
size: 524288
required: false required: false
hle_fallback: true hle_fallback: true
md5: "1e68c231d0896b7eadcad1d7d8e76129" validation: [size]
source_ref: "frontend/libretro.c:3711 (listed_bios[5])" source_ref: "frontend/libretro.c:3721-3724 (listed_bios[5])"
notes: "Searched by filename after psxonpsp660 and scph101."
- name: "scph1001.bin" - name: "scph1001.bin"
description: "SCPH-1001 (v2.2 12-04-95 A)" description: "SCPH-1001 (v2.2 12-04-95 A)"
region: "NTSC-U" region: "NTSC-U"
size: 524288
required: false required: false
hle_fallback: true hle_fallback: true
md5: "924e392ed05558ffdb115408c263dccf" validation: [size]
source_ref: "frontend/libretro.c:3711 (listed_bios[6])" source_ref: "frontend/libretro.c:3721-3724 (listed_bios[6])"
notes: "Original US model. Last in the explicit filename search list." notes: "Original US model. Last in the explicit filename search list."
# -- Dynamic scan fallback -- # -- Dynamic scan fallback --

View File

@@ -1,9 +1,12 @@
emulator: PicoDrive emulator: PicoDrive
type: libretro type: libretro
core_classification: official_port
source: "https://github.com/libretro/picodrive" 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" core_version: "1.99"
display_name: "Sega - MS/GG/MD/CD/32X (PicoDrive)" display_name: "Sega - MS/GG/MD/CD/32X (PicoDrive)"
cores: [picodrive]
systems: systems:
- sega-megadrive - sega-megadrive
- sega-genesis - sega-genesis
@@ -13,33 +16,48 @@ systems:
- sega-mastersystem - sega-mastersystem
- sega-gamegear - sega-gamegear
- sega-sg1000 - sega-sg1000
- sega-sc3000
- sega-pico - sega-pico
notes: | notes: |
PicoDrive is a fast Mega Drive / Genesis emulator with Mega CD, 32X, Master 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 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, 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 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. 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 US: us_scd2_9306, SegaCDBIOS9303, us_scd1_9210, bios_CD_U
EU: eu_mcd2_9306, eu_mcd2_9303, eu_mcd1_9210, bios_CD_E 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 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 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 external has built-in HLE that generates replacement code at startup when the BIOS
BIOS pointers are NULL (pico/32x/memory.c:2200 get_bios(), pico/32x/32x.c:172). pointers are NULL (pico/32x/memory.c:2199 get_bios(), pico/32x/32x.c:172).
The libretro frontend does not expose any 32X BIOS loading path. Only the No current frontend (libretro or standalone) loads these files; the standalone
standalone platform code references 32X_M_BIOS.BIN / 32X_S_BIOS.BIN, and loading code is disabled (#if 0 in platform/common/emu.c:1529).
that code is currently disabled (#if 0 in platform/common/emu.c:1529).
Master System, Game Gear, SG-1000: no BIOS file loaded. The core initializes carthw.cfg is a cartridge hardware database loaded from the system directory.
VDP registers and RAM to simulate post-BIOS state (pico/sms.c:1080-1096). 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: 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 # Mega CD / Sega CD - US region
# ------------------------------------------------------- # -------------------------------------------------------
@@ -48,28 +66,28 @@ files:
required: true required: true
size: 131072 # 128 KB (0x20000) size: 131072 # 128 KB (0x20000)
note: "US Sega CD Model 2 BIOS (September 1993). First in US search order." 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" - name: "SegaCDBIOS9303.bin"
system: sega-segacd system: sega-segacd
required: false required: false
size: 131072 size: 131072
note: "US Sega CD BIOS (March 1993). Second in US search order." 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" - name: "us_scd1_9210.bin"
system: sega-segacd system: sega-segacd
required: false required: false
size: 131072 size: 131072
note: "US Sega CD Model 1 BIOS (October 1992). Third in US search order." 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" - name: "bios_CD_U.bin"
system: sega-segacd system: sega-segacd
required: false required: false
size: 131072 size: 131072
note: "US Sega CD BIOS (generic name). Last in US search order." 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 # Mega CD / Sega CD - EU region
@@ -79,28 +97,28 @@ files:
required: true required: true
size: 131072 size: 131072
note: "EU Mega CD Model 2 BIOS (June 1993). First in EU search order." 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" - name: "eu_mcd2_9303.bin"
system: sega-megacd system: sega-megacd
required: false required: false
size: 131072 size: 131072
note: "EU Mega CD Model 2 BIOS (March 1993). Second in EU search order." 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" - name: "eu_mcd1_9210.bin"
system: sega-megacd system: sega-megacd
required: false required: false
size: 131072 size: 131072
note: "EU Mega CD Model 1 BIOS (October 1992). Third in EU search order." 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" - name: "bios_CD_E.bin"
system: sega-megacd system: sega-megacd
required: false required: false
size: 131072 size: 131072
note: "EU Mega CD BIOS (generic name). Last in EU search order." 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 # Mega CD / Sega CD - JP region
@@ -110,31 +128,31 @@ files:
required: true required: true
size: 131072 size: 131072
note: "JP Mega CD Model 2 BIOS (December 1992). First in JP search order." 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" - name: "jp_mcd1_9112.bin"
system: sega-megacd system: sega-megacd
required: false required: false
size: 131072 size: 131072
note: "JP Mega CD Model 1 BIOS (December 1991). Second in JP search order." 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" - name: "jp_mcd1_9111.bin"
system: sega-megacd system: sega-megacd
required: false required: false
size: 131072 size: 131072
note: "JP Mega CD Model 1 BIOS (November 1991). Third in JP search order." 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" - name: "bios_CD_J.bin"
system: sega-megacd system: sega-megacd
required: false required: false
size: 131072 size: 131072
note: "JP Mega CD BIOS (generic name). Last in JP search order." 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" - name: "32X_G_BIOS.BIN"
system: sega-32x system: sega-32x
@@ -142,7 +160,7 @@ files:
hle_fallback: true hle_fallback: true
size: 256 # 0x100 size: 256 # 0x100
note: "32X 68K (Genesis-side) BIOS. HLE replacement generated when NULL." 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" - name: "32X_M_BIOS.BIN"
system: sega-32x system: sega-32x
@@ -150,7 +168,7 @@ files:
hle_fallback: true hle_fallback: true
size: 2048 # 0x800 size: 2048 # 0x800
note: "32X Master SH2 BIOS. HLE replacement generated when NULL." 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" - name: "32X_S_BIOS.BIN"
system: sega-32x system: sega-32x
@@ -158,7 +176,7 @@ files:
hle_fallback: true hle_fallback: true
size: 1024 # 0x400 size: 1024 # 0x400
note: "32X Slave SH2 BIOS. HLE replacement generated when NULL." 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: platform_details:
megacd: megacd:
@@ -166,16 +184,16 @@ platform_details:
hle_available: false hle_available: false
region_specific: true region_specific: true
extensions_tried: [".bin", ".zip"] 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: 32x:
m68k_bios_size: 256 # 0x100 m68k_bios_size: 256 # 0x100
master_sh2_bios_size: 2048 # 0x800 master_sh2_bios_size: 2048 # 0x800
slave_sh2_bios_size: 1024 # 0x400 slave_sh2_bios_size: 1024 # 0x400
hle_available: true 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: sms:
hle_available: true hle_available: true
note: "No BIOS file loaded. VDP/RAM initialized to post-BIOS state." 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"

View File

@@ -1,7 +1,9 @@
emulator: PokeMini emulator: PokeMini
type: libretro type: libretro
core_classification: community_fork
source: "https://github.com/libretro/PokeMini" 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" core_version: "v0.60"
display_name: "Nintendo - Pokemon Mini (PokeMini)" display_name: "Nintendo - Pokemon Mini (PokeMini)"
cores: cores:
@@ -56,8 +58,6 @@ files:
required: false required: false
hle_fallback: true hle_fallback: true
size: 4096 size: 4096
md5: "1e4fb124a3a886865acb574f388c803d"
sha1: "daad4113713ed776fbd47727762bca81ba74915f"
source_ref: "source/PokeMini.c:189-206 (PokeMini_LoadBIOSFile), libretro/libretro.c:565 (path)" 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." notes: "Mapped at $000000-$000FFF (4 KB). Read via Hardware.c:144-145. Falls back to embedded FreeBIOS if missing."

View File

@@ -79,12 +79,20 @@ def md5_composite(filepath: str | Path) -> str:
names = sorted(n for n in zf.namelist() if not n.endswith("/")) names = sorted(n for n in zf.namelist() if not n.endswith("/"))
h = hashlib.md5() h = hashlib.md5()
for name in names: for name in names:
info = zf.getinfo(name)
if info.file_size > 512 * 1024 * 1024:
continue # skip oversized entries
h.update(zf.read(name)) h.update(zf.read(name))
result = h.hexdigest() result = h.hexdigest()
_md5_composite_cache[key] = result _md5_composite_cache[key] = result
return 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: def load_platform_config(platform_name: str, platforms_dir: str = "platforms") -> dict:
"""Load a platform config with inheritance and shared group resolution. """Load a platform config with inheritance and shared group resolution.
@@ -162,6 +170,7 @@ def resolve_local_file(
db: dict, db: dict,
zip_contents: dict | None = None, zip_contents: dict | None = None,
dest_hint: str = "", dest_hint: str = "",
_depth: int = 0,
) -> tuple[str | None, str]: ) -> tuple[str | None, str]:
"""Resolve a BIOS file to its local path using database.json. """Resolve a BIOS file to its local path using database.json.
@@ -293,13 +302,16 @@ def resolve_local_file(
return path, "zip_exact" return path, "zip_exact"
# MAME clone fallback: if a file was deduped, resolve via canonical # MAME clone fallback: if a file was deduped, resolve via canonical
clone_map = _get_mame_clone_map() if _depth < 3:
canonical = clone_map.get(name) clone_map = _get_mame_clone_map()
if canonical and canonical != name: canonical = clone_map.get(name)
canonical_entry = {"name": canonical} if canonical and canonical != name:
result = resolve_local_file(canonical_entry, db, zip_contents, dest_hint) canonical_entry = {"name": canonical}
if result[0]: result = resolve_local_file(
return result[0], "mame_clone" canonical_entry, db, zip_contents, dest_hint, _depth=_depth + 1,
)
if result[0]:
return result[0], "mame_clone"
return None, "not_found" 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: with zipfile.ZipFile(container) as archive:
for fname in archive.namelist(): for fname in archive.namelist():
if fname.casefold() == file_name.casefold(): if fname.casefold() == file_name.casefold():
info = archive.getinfo(fname)
if info.file_size > 512 * 1024 * 1024:
return "error"
if expected_md5 == "": if expected_md5 == "":
return "ok" return "ok"
with archive.open(fname) as entry: 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 return index
_emulator_profiles_cache: dict[tuple[str, bool], dict[str, dict]] = {}
def load_emulator_profiles( def load_emulator_profiles(
emulators_dir: str, skip_aliases: bool = True, emulators_dir: str, skip_aliases: bool = True,
) -> dict[str, dict]: ) -> 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: try:
import yaml import yaml
except ImportError: except ImportError:
@@ -385,6 +406,7 @@ def load_emulator_profiles(
if skip_aliases and profile.get("type") == "alias": if skip_aliases and profile.get("type") == "alias":
continue continue
profiles[f.stem] = profile profiles[f.stem] = profile
_emulator_profiles_cache[cache_key] = profiles
return 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: def safe_extract_zip(zip_path: str, dest_dir: str) -> None:
"""Extract a ZIP file safely, preventing zip-slip path traversal.""" """Extract a ZIP file safely, preventing zip-slip path traversal."""
dest = os.path.realpath(dest_dir) 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: if not member_path.startswith(dest + os.sep) and member_path != dest:
raise ValueError(f"Zip slip detected: {member.filename}") raise ValueError(f"Zip slip detected: {member.filename}")
zf.extract(member, dest) 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 ''})")

View File

@@ -19,6 +19,7 @@ from __future__ import annotations
import hashlib import hashlib
import struct import struct
import subprocess import subprocess
from collections.abc import Callable
from pathlib import Path from pathlib import Path
@@ -418,7 +419,7 @@ def verify_otp(
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Map from (filename, validation_type) to verification function # Map from (filename, validation_type) to verification function
_CRYPTO_VERIFIERS: dict[str, callable] = { _CRYPTO_VERIFIERS: dict[str, Callable] = {
"SecureInfo_A": verify_secure_info_a, "SecureInfo_A": verify_secure_info_a,
"LocalFriendCodeSeed_B": verify_local_friend_code_seed_b, "LocalFriendCodeSeed_B": verify_local_friend_code_seed_b,
"movable.sed": verify_movable_sed, "movable.sed": verify_movable_sed,

View File

@@ -25,10 +25,11 @@ from pathlib import Path
sys.path.insert(0, os.path.dirname(__file__)) sys.path.insert(0, os.path.dirname(__file__))
from common import ( from common import (
build_zip_contents_index, check_inside_zip, compute_hashes, _build_validation_index, build_zip_contents_index, check_file_validation,
group_identical_platforms, load_database, load_data_dir_registry, check_inside_zip, compute_hashes, filter_files_by_mode,
load_emulator_profiles, load_platform_config, md5_composite, group_identical_platforms, list_emulator_profiles, list_system_ids,
resolve_local_file, load_database, load_data_dir_registry, load_emulator_profiles,
load_platform_config, md5_composite, resolve_local_file,
) )
from deterministic_zip import rebuild_zip_deterministic from deterministic_zip import rebuild_zip_deterministic
@@ -256,7 +257,6 @@ def generate_pack(
file_reasons: dict[str, str] = {} file_reasons: dict[str, str] = {}
# Build emulator-level validation index (same as verify.py) # Build emulator-level validation index (same as verify.py)
from verify import _build_validation_index
validation_index = {} validation_index = {}
if emu_profiles: if emu_profiles:
validation_index = _build_validation_index(emu_profiles) validation_index = _build_validation_index(emu_profiles)
@@ -367,7 +367,6 @@ def generate_pack(
# In md5 mode: validation downgrades OK to UNTESTED # In md5 mode: validation downgrades OK to UNTESTED
if (file_status.get(dedup_key) == "ok" if (file_status.get(dedup_key) == "ok"
and local_path and validation_index): and local_path and validation_index):
from verify import check_file_validation
fname = file_entry.get("name", "") fname = file_entry.get("name", "")
reason = check_file_validation(local_path, fname, validation_index) reason = check_file_validation(local_path, fname, validation_index)
if reason: 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 # 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, def _resolve_destination(file_entry: dict, pack_structure: dict | None,
standalone: bool) -> str: standalone: bool) -> str:
"""Resolve the ZIP destination path for a file entry.""" """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: with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
for emu_name, profile in sorted(selected): for emu_name, profile in sorted(selected):
pack_structure = profile.get("pack_structure") 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", []): for dd in profile.get("data_directories", []):
ref_key = dd.get("ref", "") ref_key = dd.get("ref", "")
if not ref_key or not data_registry or ref_key not in data_registry: 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 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]: def list_platforms(platforms_dir: str) -> list[str]:
"""List available platform names from YAML files.""" """List available platform names from YAML files."""
platforms = [] platforms = []
@@ -893,10 +851,10 @@ def main():
print(p) print(p)
return return
if args.list_emulators: if args.list_emulators:
_list_emulators_pack(args.emulators_dir) list_emulator_profiles(args.emulators_dir)
return return
if args.list_systems: if args.list_systems:
_list_systems_pack(args.emulators_dir) list_system_ids(args.emulators_dir)
return return
# Mutual exclusion # 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": if name.startswith("INSTRUCTIONS_") or name == "manifest.json":
continue continue
with zf.open(info) as f: with zf.open(info) as f:
data = f.read() sha1_h = hashlib.sha1()
sha1 = hashlib.sha1(data).hexdigest() md5_h = hashlib.md5()
md5 = hashlib.md5(data).hexdigest() size = 0
size = len(data) 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 # Look up in database: files_db keyed by SHA1
db_entry = files_db.get(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: def inject_manifest(zip_path: str, manifest: dict) -> None:
"""Inject manifest.json into an existing ZIP pack.""" """Inject manifest.json into an existing ZIP pack."""
import tempfile as _tempfile
manifest_json = json.dumps(manifest, indent=2, ensure_ascii=False) manifest_json = json.dumps(manifest, indent=2, ensure_ascii=False)
# ZipFile doesn't support appending to existing entries, # Check if manifest already exists
# so we rebuild with the manifest added with zipfile.ZipFile(zip_path, "r") as zf:
tmp_fd, tmp_path = _tempfile.mkstemp(suffix=".zip", dir=os.path.dirname(zip_path)) has_manifest = "manifest.json" in zf.namelist()
os.close(tmp_fd)
try: if not has_manifest:
with zipfile.ZipFile(zip_path, "r") as src, \ # Fast path: append directly
zipfile.ZipFile(tmp_path, "w", zipfile.ZIP_DEFLATED) as dst: with zipfile.ZipFile(zip_path, "a") as zf:
for item in src.infolist(): zf.writestr("manifest.json", manifest_json)
if item.filename == "manifest.json": else:
continue # replace existing # Rebuild to replace existing manifest
dst.writestr(item, src.read(item.filename)) import tempfile as _tempfile
dst.writestr("manifest.json", manifest_json) tmp_fd, tmp_path = _tempfile.mkstemp(suffix=".zip", dir=os.path.dirname(zip_path))
os.replace(tmp_path, zip_path) os.close(tmp_fd)
except Exception: try:
os.unlink(tmp_path) with zipfile.ZipFile(zip_path, "r") as src, \
raise 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: def generate_sha256sums(output_dir: str) -> str | None:

View File

@@ -198,11 +198,22 @@ def _download_and_extract(
shutil.copyfileobj(src, dst) shutil.copyfileobj(src, dst)
file_count += 1 file_count += 1
# atomic swap: remove old cache, move new into place # atomic swap: rename old before moving new into place
if cache_dir.exists():
shutil.rmtree(cache_dir)
cache_dir.parent.mkdir(parents=True, exist_ok=True) 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 return file_count

View File

@@ -194,6 +194,7 @@ class Scraper(BaseScraper):
"""Scraper for libretro-core-info firmware declarations.""" """Scraper for libretro-core-info firmware declarations."""
def __init__(self): def __init__(self):
super().__init__()
self._info_files: dict[str, dict] | None = None self._info_files: dict[str, dict] | None = None
def _fetch_info_list(self) -> list[str]: def _fetch_info_list(self) -> list[str]:

View File

@@ -185,6 +185,7 @@ class Scraper(BaseScraper):
"""Scraper for EmuDeck checkBIOS.sh and CSV cheat sheets.""" """Scraper for EmuDeck checkBIOS.sh and CSV cheat sheets."""
def __init__(self, checkbios_url: str = CHECKBIOS_URL, csv_base_url: str = CSV_BASE_URL): 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.checkbios_url = checkbios_url
self.csv_base_url = csv_base_url self.csv_base_url = csv_base_url
self._raw_script: str | None = None self._raw_script: str | None = None

View File

@@ -93,7 +93,10 @@ class ValidationResult:
def load_database(db_path: str) -> dict | None: def load_database(db_path: str) -> dict | None:
try: try:
return _load_database(db_path) 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 return None

View File

@@ -35,13 +35,12 @@ except ImportError:
sys.path.insert(0, os.path.dirname(__file__)) sys.path.insert(0, os.path.dirname(__file__))
from common import ( from common import (
build_zip_contents_index, check_inside_zip, compute_hashes, _build_validation_index, build_zip_contents_index, check_file_validation,
group_identical_platforms, load_data_dir_registry, check_inside_zip, compute_hashes, filter_files_by_mode,
load_emulator_profiles, load_platform_config, 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, md5sum, md5_composite, resolve_local_file, resolve_platform_cores,
) )
from crypto_verify import check_crypto_validation
DEFAULT_DB = "database.json" DEFAULT_DB = "database.json"
DEFAULT_PLATFORMS_DIR = "platforms" DEFAULT_PLATFORMS_DIR = "platforms"
DEFAULT_EMULATORS_DIR = "emulators" 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} _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 # Verification functions
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -269,7 +101,7 @@ def verify_entry_md5(
base = {"name": name, "required": required} base = {"name": name, "required": required}
if expected_md5 and "," in expected_md5: 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: else:
md5_list = [expected_md5] if expected_md5 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 # 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: def _effective_validation_label(details: list[dict], validation_index: dict) -> str:
"""Determine the bracket label for the report. """Determine the bracket label for the report.
@@ -783,7 +602,7 @@ def verify_emulator(
data_dir_notices: list[str] = [] data_dir_notices: list[str] = []
for emu_name, profile in selected: 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) # Check data directories (only notice if not cached)
for dd in profile.get("data_directories", []): 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)") 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(): def main():
parser = argparse.ArgumentParser(description="Platform-native BIOS verification") parser = argparse.ArgumentParser(description="Platform-native BIOS verification")
parser.add_argument("--platform", "-p", help="Platform name") parser.add_argument("--platform", "-p", help="Platform name")
@@ -1021,10 +812,10 @@ def main():
args = parser.parse_args() args = parser.parse_args()
if args.list_emulators: if args.list_emulators:
_list_emulators(args.emulators_dir) list_emulator_profiles(args.emulators_dir)
return return
if args.list_systems: if args.list_systems:
_list_systems(args.emulators_dir) list_system_ids(args.emulators_dir)
return return
# Mutual exclusion # Mutual exclusion

View File

@@ -29,14 +29,15 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "scripts"))
import yaml import yaml
from common import ( from common import (
build_zip_contents_index, check_inside_zip, group_identical_platforms, _build_validation_index, build_zip_contents_index, check_file_validation,
load_emulator_profiles, load_platform_config, md5_composite, md5sum, check_inside_zip, compute_hashes, filter_files_by_mode,
resolve_local_file, resolve_platform_cores, 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 ( from verify import (
Severity, Status, verify_platform, find_undeclared_files, find_exclusion_notes, Severity, Status, verify_platform, find_undeclared_files, find_exclusion_notes,
_build_validation_index, check_file_validation, verify_emulator, verify_emulator, _effective_validation_label,
_filter_files_by_mode, _effective_validation_label,
) )
@@ -58,6 +59,10 @@ class TestE2E(unittest.TestCase):
# --------------------------------------------------------------- # ---------------------------------------------------------------
def setUp(self): 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.root = tempfile.mkdtemp()
self.bios_dir = os.path.join(self.root, "bios") self.bios_dir = os.path.join(self.root, "bios")
self.platforms_dir = os.path.join(self.root, "platforms") 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 # test_validation has crc32, md5, sha1, size → all listed
self.assertEqual(result["verification_mode"], "crc32+md5+sha1+signature+size") self.assertEqual(result["verification_mode"], "crc32+md5+sha1+signature+size")
def test_99_filter_files_by_mode(self): def test_99filter_files_by_mode(self):
"""_filter_files_by_mode correctly filters standalone/libretro.""" """filter_files_by_mode correctly filters standalone/libretro."""
files = [ files = [
{"name": "a.bin"}, # no mode → both {"name": "a.bin"}, # no mode → both
{"name": "b.bin", "mode": "libretro"}, # libretro only {"name": "b.bin", "mode": "libretro"}, # libretro only
{"name": "c.bin", "mode": "standalone"}, # standalone only {"name": "c.bin", "mode": "standalone"}, # standalone only
{"name": "d.bin", "mode": "both"}, # explicit both {"name": "d.bin", "mode": "both"}, # explicit both
] ]
lr = _filter_files_by_mode(files, standalone=False) lr = filter_files_by_mode(files, standalone=False)
sa = _filter_files_by_mode(files, standalone=True) sa = filter_files_by_mode(files, standalone=True)
lr_names = {f["name"] for f in lr} lr_names = {f["name"] for f in lr}
sa_names = {f["name"] for f in sa} sa_names = {f["name"] for f in sa}
self.assertEqual(lr_names, {"a.bin", "b.bin", "d.bin"}) 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) 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__": if __name__ == "__main__":
unittest.main() unittest.main()