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)
# ref: libretro/lrps2, docs.libretro.com/library/lrps2
# Same BIOS as PCSX2 standalone, placed in pcsx2/bios/ subfolder.
# Also needs GameIndex.yaml for game compatibility database.
#
# doc vs source: docs say "no specific filename required" for BIOS, any
# valid 4 MB PS2 BIOS dump works. Docs do not list GameIndex.yaml as a
# BIOS file, but the source code loads it from pcsx2/resources/ and it
# is needed for game-specific patches and compatibility fixes.
# SCPH-10000 not recommended per docs (lower compatibility).
# LRPS2 - libretro PlayStation 2 core
# Source: https://github.com/libretro/ps2
# Upstream: https://github.com/PCSX2/pcsx2
emulator: LRPS2
type: libretro
core_classification: community_fork
source: "https://github.com/libretro/ps2"
upstream: "https://github.com/PCSX2/pcsx2"
profiled_date: "2026-03-24"
core_version: "Git"
display_name: "Sony - PlayStation 2 (LRPS2)"
cores: [lrps2]
source: "https://github.com/libretro/lrps2"
systems:
- sony-playstation-2
systems: [sony-playstation-2]
bios_directory: "pcsx2/bios/"
resources_directory: "pcsx2/resources/"
notes: |
Libretro port of PCSX2. Uses the same BIOS files as standalone PCSX2.
BIOS must NOT be zipped. Any valid PS2 BIOS works (SCPH-10000 not
recommended due to lower compatibility). BIOS goes in pcsx2/bios/.
GameIndex.yaml goes in pcsx2/resources/ and is needed for proper
game-specific settings (patches, fixes).
Hard fork of PCSX2 ported to libretro. BIOS detection is filename-agnostic: the core
scans pcsx2/bios/ for any file between 4-8 MB with a valid romdir structure containing
RESET and ROMVER entries. The ROMVER entry determines region and version.
Companion files (.rom1, .rom2, .nvm, .mec) derive their paths from the selected BIOS.
DEV9 (network adapter) and USB are stubbed in the libretro port.
GameIndex.yaml in pcsx2/resources/ provides per-game patches for compatibility.
files:
- name: GameIndex.yaml
system: sony-playstation-2
- name: "<bios>.bin"
path: "pcsx2/bios/"
required: true
note: "game compatibility database, not a BIOS — pcsx2/resources/GameIndex.yaml"
source_ref: "pcsx2/GameIndex.yaml"
description: "PS2 BIOS binary"
min_size: 4194304
max_size: 8388608
validation: [size]
source_ref: "pcsx2/ps2/BiosTools.cpp:230-254,266-322"
note: >
Scans pcsx2/bios/ for any file between 4 MB and 8 MB. Validates via romdir
structure parsing (RESET + ROMVER entries). User selects BIOS via core option
pcsx2_bios. Falls back to first valid BIOS found if configured BIOS is missing.
- name: "<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
core_classification: enhanced_fork
source: "https://github.com/libretro/mupen64plus-libretro-nx"
upstream: "https://github.com/mupen64plus/mupen64plus-core"
profiled_date: "2026-03-24"
core_version: "2.8"
core_version: "2.6.0"
display_name: "Nintendo - Nintendo 64 (Mupen64Plus-Next)"
systems: [nintendo-64, nintendo-64dd]
cores: [mupen64plus_next, mupen64plus_next_develop, mupen64plus_next_gles3, mupen64plus_next_gles2]
@@ -14,8 +14,16 @@ files:
path: "Mupen64plus/IPL.n64"
size: 4194304
required: false
note: "64DD IPL ROM. Only needed for N64 Disk Drive games (.ndd) via subsystem API"
source_ref: "mupen64plus-core/src/main/main.c:954-979"
description: "64DD IPL ROM"
note: "Only needed for N64 Disk Drive games (.ndd) via subsystem API. Accepts Z64, N64, and V64 byte-swap formats."
source_ref: "mupen64plus-core/src/main/main.c:940-1024"
- name: "font.ttf"
path: "Mupen64plus/font.ttf"
required: false
description: "TrueType font for on-screen display text"
note: "Fallback font for OSD text rendering (FPS counter, messages). System fonts tried first. No impact on emulation."
source_ref: "GLideN64/src/TextDrawer.cpp:165-170"
notes:
hle_available: true
@@ -29,3 +37,6 @@ notes:
transferpak_note: >
Transfer Pak support (GB/GBC games on N64) handled via subsystem API.
No additional firmware files needed.
embedded_data: >
mupen64plus.ini (ROM database) and GLideN64.custom.ini (per-game GPU
settings) are embedded as compiled headers and auto-generated at init.

View File

@@ -1,8 +1,149 @@
emulator: "nekop2"
type: alias
alias_of: "np2kai"
profiled_date: "2026-03-18"
emulator: nekop2
type: libretro
core_classification: community_fork
source: "https://github.com/libretro/libretro-meowPC98"
upstream: "https://np2.yui.ne.jp (dead; source preserved in libretro repo)"
profiled_date: "2026-03-24"
core_version: "0.86"
display_name: "NEC - PC-98 (Neko Project II)"
note: "This core uses the same BIOS/firmware as np2kai. See emulators/np2kai.yml for details."
files: []
cores: [nekop2]
systems: [pc-98]
# Neko Project II (NP2) by Yui, ported to libretro by meepingsnesroms.
# PC-9801/9821 emulator (80286/IA-32 CPU). All files load from <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
type: libretro
core_classification: community_fork
source: "https://github.com/libretro/nestopia"
upstream: "https://gitlab.com/jgemu/nestopia"
logo: "https://raw.githubusercontent.com/0ldsk00l/nestopia/master/icons/svg/nestopia.svg"
profiled_date: "2026-03-18"
profiled_date: "2026-03-24"
core_version: "1.53.1"
display_name: "Nintendo - NES / Famicom (Nestopia)"
cores: [nestopia]
systems: [nintendo-nes, nintendo-fds]
notes: |
Nestopia UE (Undead Edition) is a cycle-accurate NES/Famicom emulator.
NES cartridge games need no BIOS. Famicom Disk System games require
the FDS BIOS ROM (disksys.rom, 8 KB) loaded from the system directory.
The core validates the BIOS via CRC32 against two known dumps:
standard Famicom (0x5E607DCF) and Twin Famicom (0x4DF24A6C).
An unknown BIOS triggers a warning but still loads.
Nestopia UE is a cycle-accurate NES/Famicom emulator. The libretro port
tracks the upstream Nestopia JG project. NES cartridge games need no BIOS.
Famicom Disk System games require disksys.rom (8 KB FDS BIOS) in the
system directory. The core validates the BIOS via CRC32 against two known
dumps: standard Famicom (0x5E607DCF) and Twin Famicom (0x4DF24A6C). An
unknown BIOS triggers a warning but still loads.
NstDatabase.xml is an optional game database used for region autodetection,
aspect ratio selection, and 4-player adapter recognition. A copy is baked
into the core binary (libretro/nstdatabase.hpp) and used as fallback when
no external file is found. Placing a newer NstDatabase.xml in the system
directory overrides the built-in copy.
NstDatabase.xml is an optional game database for region autodetection,
mapper selection, and 4-player adapter recognition. A copy is baked into
the core binary (libretro/nstdatabase.hpp) as fallback. The upstream
Nestopia JG requires this file; the libretro port makes it optional.
An optional custom palette file (custom.pal, 192 bytes, 64 RGB triplets)
overrides the built-in palettes when the nestopia_palette core option is
set to Custom. The core ships with 10+ built-in palettes (Royaltea,
Smooth FBx, etc.) so the external file is rarely needed.
An optional custom palette file (custom.pal, 64 RGB triplets) overrides
built-in palettes when the nestopia_palette core option is set to Custom.
The libretro port embeds all named palettes (Royaltea, Smooth FBx, etc.)
that the upstream loads from external .pal files.
Audio samples for specific games can be placed in system/nestopia/samples/
as numbered .wav files (e.g., 00.wav, 01.wav). This is used for Famicom
expansion audio in a few titles.
Game Genie is handled as cheat code decoding (software), not via a ROM file.
File path construction: libretro/libretro.cpp retro_load_game() joins the
system directory (RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY) with each
filename directly - no subdirectories.
Five Famicom games use external ADPCM audio samples placed in
system/nestopia/samples/{game}/ as numbered .wav files. These originate
from MAME sample ZIPs. Games function without them but miss some audio.
files:
# --- Famicom Disk System BIOS (required for FDS games) ---
@@ -43,6 +39,7 @@ files:
description: "FDS BIOS ROM"
required: true
size: 8192
validation: [crc32]
md5: "ca30b50f880eb660a320674ed365ef7a"
sha1: "57fe1bdee955bb48d357e463ccbf129496930b62"
source_ref: "libretro/libretro.cpp:1608-1634 (FDS load), source/core/NstFds.cpp:61-131 (Bios class, CRC32 validation)"
@@ -54,11 +51,8 @@ files:
description: "Nestopia game database for region and mapper autodetection"
required: false
hle_fallback: true
size: 1022369
md5: "0ee6cbdc6f5c96ce9c8aa5edb59066f4"
sha1: ~
source_ref: "libretro/libretro.cpp:1561-1586 (database load), libretro/nstdatabase.hpp (baked-in fallback)"
notes: "XML database matching games by SHA1+CRC32. Used for region, mapper, mirroring, and 4-player adapter detection. Built-in fallback exists so this file is optional. Hash is for the upstream copy shipped with the core repo."
notes: "XML database matching games by SHA1+CRC32. Used for region, mapper, mirroring, and 4-player adapter detection. Built-in fallback exists so this file is optional."
# --- Custom palette (optional, core option nestopia_palette = Custom) ---
- name: "custom.pal"
@@ -67,7 +61,41 @@ files:
required: false
hle_fallback: true
size: 192
md5: ~
sha1: ~
source_ref: "libretro/libretro.cpp:1540-1559 (palette load)"
notes: "64 RGB triplets (64 x 3 bytes = 192 bytes). Only loaded when nestopia_palette core option is set to Custom. Falls back to built-in Royaltea palette if not found. Multiple valid palettes exist so no single canonical hash."
notes: "64 RGB triplets (64 x 3 bytes = 192 bytes). Loaded unconditionally at startup; applied only when nestopia_palette core option is set to Custom. Falls back to built-in Royaltea palette. Multiple valid palettes exist so no canonical hash."
# --- ADPCM audio samples (optional, game-specific) ---
- name: "nestopia/samples/moepro/"
system: nintendo-nes
description: "Moero Pro Yakyuu audio samples (16 .wav files, 00.wav-15.wav)"
required: false
category: game_data
source_ref: "libretro/libretro.cpp:142-180 (load_wav), libretro/libretro.cpp:247 (LOAD_SAMPLE_MOERO_PRO_YAKYUU callback), source/core/NstSoundPlayer.hpp:53 (16 samples)"
- name: "nestopia/samples/moepro88/"
system: nintendo-nes
description: "Moero Pro Yakyuu '88 audio samples (20 .wav files, 00.wav-19.wav)"
required: false
category: game_data
source_ref: "libretro/libretro.cpp:142-180 (load_wav), libretro/libretro.cpp:249 (LOAD_SAMPLE_MOERO_PRO_YAKYUU_88 callback), source/core/NstSoundPlayer.hpp:54 (20 samples)"
- name: "nestopia/samples/mptennis/"
system: nintendo-nes
description: "Moero Pro Tennis audio samples (19 .wav files, 00.wav-18.wav)"
required: false
category: game_data
source_ref: "libretro/libretro.cpp:142-180 (load_wav), libretro/libretro.cpp:251 (LOAD_SAMPLE_MOERO_PRO_TENNIS callback), source/core/NstSoundPlayer.hpp:55 (19 samples)"
- name: "nestopia/samples/terao/"
system: nintendo-nes
description: "Terao no Dosukoi Oozumou audio samples (6 .wav files, 00.wav-05.wav)"
required: false
category: game_data
source_ref: "libretro/libretro.cpp:142-180 (load_wav), libretro/libretro.cpp:253 (LOAD_SAMPLE_TERAO_NO_DOSUKOI_OOZUMOU callback), source/core/NstSoundPlayer.hpp:56 (6 samples)"
- name: "nestopia/samples/ftaerobi/"
system: nintendo-nes
description: "Aerobics Studio audio samples (8 .wav files, 00.wav-07.wav)"
required: false
category: game_data
source_ref: "libretro/libretro.cpp:142-180 (load_wav), libretro/libretro.cpp:255 (LOAD_SAMPLE_AEROBICS_STUDIO callback), source/core/NstSoundPlayer.hpp:57 (8 samples)"

View File

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

View File

@@ -1,7 +1,9 @@
emulator: Numero
type: libretro
core_classification: community_fork
source: "https://github.com/nbarkhina/numero"
profiled_date: "2026-03-18"
upstream: "https://github.com/sputt/wabbitemu"
profiled_date: "2026-03-24"
core_version: "v1.0"
display_name: "Texas Instruments TI-83 (Numero)"
cores:
@@ -10,38 +12,35 @@ systems:
- ti-83
notes: |
Numero is a TI-83 family calculator emulator for libretro, based on the
Wabbitemu emulator. Supports TI-83, TI-83 Plus and TI-83 Silver Edition.
Libretro port of Wabbitemu, a TI-83 family calculator emulator.
Supports TI-83, TI-83 Plus and TI-83 Plus Silver Edition.
A calculator ROM dump is required to run. The core searches for ROM files
in the system directory root with a fixed priority order
(libretronew.cpp:575-594):
A calculator ROM dump is required. The core searches sequentially in the
system directory (libretronew.cpp:575-594): ti83se.rom first, then
ti83plus.rom, then ti83.rom. The first found is loaded via rom_load()
(libretronew.cpp:608). file_present_in_system() checks existence only
via VFS open/close (libretronew.cpp:451-472). No hash or size validation.
1. ti83se.rom (TI-83 Silver Edition, recommended -- largest storage)
2. ti83plus.rom (TI-83 Plus)
3. ti83.rom (TI-83)
Without any ROM, the core shows a blank screen listing the expected
filenames (libretronew.cpp:1030-1042). No HLE fallback.
The first file found wins. file_present_in_system() checks existence only
(libretronew.cpp:451-472), then rom_load() loads it (libretronew.cpp:608).
Supports no-content launch (libretronew.cpp:556). Content files
(.8xp/.8xk/.8xg) are loaded on top of the running ROM via link cable
simulation. Auto-saves calculator state every 10 seconds
(libretronew.cpp:1050-1063).
There is no fallback or built-in ROM. If none of the three files are found,
the core starts without a BIOS and shows a blank screen with a status
message listing the expected filenames (libretronew.cpp:1034-1041).
The core supports no-content launch (RETRO_ENVIRONMENT_SET_SUPPORT_NO_GAME
is set to true, libretronew.cpp:555-556). Content files are .8xp/.8xk/.8xg
calculator programs loaded on top of the running ROM.
The emulator auto-saves calculator state every 10 seconds to preserve RAM
contents (the TI-83 has no persistent storage beyond battery-backed RAM).
Upstream Wabbitemu supports a wider model range (TI-73 through TI-84+CSE)
but the libretro wrapper only searches for TI-83 family filenames. The
underlying engine (calc.cpp:rom_load) auto-detects the model from the ROM
header.
files:
- name: "ti83se.rom"
system: ti-83
description: "TI-83 Silver Edition ROM dump"
description: "TI-83 Plus Silver Edition ROM dump"
required: false
source_ref: "libretronew.cpp:576-578"
notes: "Checked first. Recommended by the author for largest storage capacity."
notes: "Checked first. Recommended for largest storage capacity."
- name: "ti83plus.rom"
system: ti-83
@@ -55,28 +54,4 @@ files:
description: "TI-83 ROM dump"
required: false
source_ref: "libretronew.cpp:591-593"
notes: "Checked last. Original TI-83 (non-Plus) ROM."
platform_details:
bios_search:
source_ref: "libretronew.cpp:451-472, 575-608"
notes: |
file_present_in_system() opens a read handle via libretro VFS to
test file existence. The search is sequential: ti83se.rom first,
then ti83plus.rom, then ti83.rom. Only one ROM is loaded.
rom_loading:
source_ref: "src/Interface/calc.cpp (rom_load)"
notes: |
rom_load() detects the calculator model from the ROM image header
and initializes the appropriate hardware (83, 83+, 83SE). Each
model gets its own progress/save directory via setProgressDir().
content_loading:
formats: "8xp, 8xk, 8xg"
source_ref: "libretronew.cpp:483, 769"
notes: |
Calculator programs (.8xp), apps (.8xk) and groups (.8xg) are
loaded into the running calculator via the sendfile mechanism,
simulating the link cable transfer. Multiple programs can be
loaded sequentially onto the same ROM session.
notes: "Checked last. Original TI-83 (non-Plus)."

View File

@@ -1,45 +1,38 @@
emulator: NXEngine
type: libretro
core_classification: game_engine
source: "https://github.com/libretro/nxengine-libretro"
profiled_date: "2026-03-18"
upstream: "https://github.com/Rox64/NXEngine"
profiled_date: "2026-03-24"
core_version: "1.0.0.6"
display_name: "Cave Story (NXEngine)"
cores: [nxengine]
systems: [cave-story]
notes: |
NXEngine is an open-source reimplementation of the Cave Story (Doukutsu
Monogatari) engine by Studio Pixel. It is not an emulator but a source port
that loads the original freeware game data directly.
Monogatari) engine by Studio Pixel, authored by Caitlin Shaw. It is not
an emulator but a source port that loads the original freeware game data.
The core requires the original freeware Cave Story distribution placed in
{system_dir}/nxengine/. When launched without a content file, it looks for
Doukutsu.exe at that path (libretro.cpp:254-259). When launched with a .exe
content file, it uses the parent directory of that file instead
(libretro.cpp:244).
The core requires the freeware Cave Story distribution placed in
{system_dir}/nxengine/. When launched without content, it looks for
Doukutsu.exe there (libretro.cpp:254-259). When launched with a .exe
content file, it uses the parent directory of that file (libretro.cpp:237).
At startup (main.cpp:66-90), the core opens Doukutsu.exe and extracts from
it: ORG music files (extractorg.c), PXT sound effects (extractpxt.c), stage
tile attributes (extractstages.c), bitmap graphics, and the wavetable
(cachefiles.c:484). These are kept in memory, not written to disk.
At startup (main.cpp:75-90), the core opens Doukutsu.exe and extracts:
ORG music (extractorg.c), PXT sound effects (extractpxt.c), stage tile
attributes (extractstages.c), 18 credit BMPs + pixel.bmp (endpic/),
and the wavetable (cachefiles.c:464-485). All kept in memory.
The data/ directory must exist alongside Doukutsu.exe with the full game
asset tree: 30 root-level files (sprites, backgrounds, script tables),
36 NPC sprite sheets in data/Npc/, and 333 stage files in data/Stage/
(maps .pxm, tile attributes .pxa, entity lists .pxe, scripts .tsc, and
tileset images via Prt*.pbm). The core verifies data/ exists by checking
for data/npc.tbl (main.cpp:47-63).
The data/ directory (399 files from the freeware release) is loaded into
an in-memory cache at init (cachefiles.c:38-439, 497-540). All subsequent
file access goes through this cache. The core verifies data/ by checking
for data/npc.tbl (main.cpp:47-58).
The core also reads endpic/ bitmaps for ending sequences, extracted from
Doukutsu.exe at runtime (cachefiles.c:460-484).
The font is compiled-in (bitmap_font.h). The upstream standalone version
uses font.ttf via SDL_ttf.
All required files ship in the datafiles/ directory of the libretro repo
itself, since Cave Story is freeware. No separate BIOS or firmware is
needed. The entire freeware distribution is the "system" requirement.
sprites.sif has a compiled-in fallback (sprites_sif.h) and is the only
data file that can be missing without a fatal error (cachefiles.c:515-521).
Valid content extension: .exe (retro_get_system_info, libretro.cpp:96).
Valid content extension: .exe (libretro.cpp:109).
files:
- name: "Doukutsu.exe"
@@ -51,20 +44,37 @@ files:
md5: "38695d3d69d7a0ada8178072dad4c58b"
sha1: "bb2d0441e073da9c584f23c2ad8c7ab8aac293bf"
source_ref: "main.cpp:77-78 (opened for extraction), libretro.cpp:258 (existence check)"
notes: "Placed in system/nxengine/. The core extracts ORG music, PXT sounds, stage tile attributes, BMP graphics, and wavetable.dat from this binary at each launch."
- name: "data/npc.tbl"
category: game_data
system: cave-story
description: "NPC attribute table (entity behavior flags, HP, damage, display rect offsets)"
required: true
source_ref: "main.cpp:50 (existence check for data/ directory validation)"
notes: "Located in system/nxengine/data/. The core uses this file to verify the data directory is present."
notes: "Placed in system/nxengine/. The core extracts ORG music, PXT sounds, stage tile attributes, BMP graphics, and wavetable from this binary at each launch."
- name: "data/"
category: game_data
system: cave-story
description: "Full game asset directory tree (399 files: sprites, NPC sheets, stage maps, scripts, backgrounds)"
required: true
source_ref: "cachefiles.c:38-480 (complete file list loaded at init)"
source_ref: "cachefiles.c:38-439 (filenames[] array loaded at init)"
notes: "Must contain root assets (Arms.pbm, MyChar.pbm, etc.), Npc/ (36 sprite sheets), and Stage/ (333 map/script/tileset files). All files from the original freeware release."
- name: "data/npc.tbl"
category: game_data
system: cave-story
description: "NPC attribute table (entity behavior flags, HP, damage, display rect offsets)"
required: true
source_ref: "main.cpp:50 (existence check for data/ directory), ai/ai.cpp:56-59 (loaded for NPC properties)"
notes: "Located in system/nxengine/data/. Used to validate data directory presence and to load NPC behavior attributes."
- name: "data/sprites.sif"
category: game_data
system: cave-story
description: "Sprite information file (sprite positions, sizes, animation data)"
required: false
hle_fallback: true
source_ref: "cachefiles.c:101 (in filenames[]), cachefiles.c:515-521 (compiled-in fallback from sprites_sif.h)"
notes: "Not shipped in the freeware distribution. If missing, the core uses a compiled-in copy (sprites_sif.h)."
- name: "tilekey.dat"
category: game_data
system: cave-story
description: "Tile attribute lookup table (maps tile codes to collision/behavior attributes)"
required: false
source_ref: "map.cpp:290-303 (loaded at init, hardcoded default if missing)"
notes: "Not part of the freeware distribution. Generated by the standalone NXEngine extraction tool. The libretro core has hardcoded defaults in map.cpp:30."

View File

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

View File

@@ -1,9 +1,10 @@
emulator: Oberon
type: libretro
core_classification: community_fork
source: "https://github.com/libretro/oberon-risc-emu"
profiled_date: "2026-03-18"
upstream: "https://github.com/pdewacht/oberon-risc-emu"
profiled_date: "2026-03-24"
core_version: "2020-07-01"
display_name: "Project Oberon RISC"
display_name: "Oberon RISC Emulator"
cores:
- oberon
@@ -11,33 +12,15 @@ systems: [oberon]
# Project Oberon RISC emulator by Peter De Wachter.
# Emulates the Oberon RISC processor designed by Niklaus Wirth.
# The bootloader (512 words) is compiled into the binary from risc-boot.inc,
# loaded into ROM at 0xFFFFF800 on startup (risc.c:75-77, risc_new).
#
# Content: .dsk disk images containing the full Oberon operating system.
# The disk image is loaded via retro_load_game(game->path) and attached
# as SPI disk (Libretro/libretro.c:209-214). No files are read from
# the RetroArch system directory.
#
# Reference disk images ship in the repo under DiskImage/:
# Oberon-2020-08-18.dsk (990208 bytes, latest)
# Oberon-2019-01-21.dsk (988160 bytes)
# Oberon-2016-08-02.dsk (989184 bytes)
# Oberon-2016-04-18.dsk (989184 bytes)
# These are game content, not system files.
# Bootloader (512 words) compiled into the binary from risc-boot.inc,
# loaded into ROM at 0xFFFFF800 on startup (risc.c:75-77,93).
# No retro_get_system_directory() call — no files from system dir.
# Content: .dsk disk images loaded via retro_load_game (libretro.c:209-214).
# Upstream and libretro core emulation code (risc.c, disk.c) are identical.
files: []
notes:
architecture: >
Custom 32-bit RISC CPU (25 MHz emulated) with 1 MB RAM (expandable to 32 MB).
Monochrome 1-bit framebuffer. Keyboard input via PS/2 scancodes.
SPI bus for SD card (disk image) access. Serial port for PCLink file transfer.
boot_process: >
CPU starts execution at ROM address 0xFFFFF800. The embedded bootloader
reads the boot sector from the SPI disk and loads the Oberon inner core
(modules Kernel, FileDir, Files, Modules) into RAM, then jumps to it.
content_format: >
Disk images (.dsk) are raw sector images read via 512-byte SPI commands.
The core detects filesystem-only images (magic 0x9B1EA38D at sector 0)
and adjusts the sector offset accordingly (disk.c:57-58).
Bootloader embedded in ROM reads boot sector from SPI disk, loads
Oberon inner core into RAM. Content is raw sector .dsk images.

View File

@@ -1,7 +1,9 @@
emulator: ONScripter
type: libretro
core_classification: community_fork
source: "https://github.com/iyzsong/onscripter-libretro"
profiled_date: "2026-03-18"
upstream: "https://github.com/ogapee/onscripter"
profiled_date: "2026-03-24"
core_version: "0.3"
display_name: "ONScripter"
cores:
@@ -10,30 +12,18 @@ systems:
- onscripter
notes: |
ONScripter is a clone of NScripter, a Japanese visual novel engine by
Naoki Takahashi. The libretro port by iyzsong wraps the upstream engine
(ogapee/onscripter, tag 20230825) with an SDL-to-libretro shim layer.
Clone of NScripter, a Japanese visual novel engine. The libretro port
wraps upstream ogapee/onscripter (tag 20230825) via an SDL-to-libretro
shim.
No BIOS or system files are required. The .info file declares no
firmware entries and the core never calls
RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY.
No system files required. The .info declares no firmware. The core
never calls RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY. All files
(scripts, fonts, archives, config) are loaded from the game directory
via archive_path (libretro.cpp:120-121, ScriptHandler.cpp:148-161).
Game content is loaded directly from the archive path containing the
script file. The core opens txt, dat, ___, or ons script files via
ONScripter::openScript() and reads all game assets (graphics, audio,
fonts) from the same directory.
default.ttf, registry.txt, dll.txt are per-game assets shipped with
each visual novel, not system-level files.
The engine looks for a font file named default.ttf in the game
directory. This is a per-game asset shipped with each visual novel,
not a system-level file managed by RetroArch.
Dependencies (bundled at build time via Guix): freetype, libjpeg,
libogg, libvorbis, libmad, bzip2, SDL (shimmed to libretro).
The core is marked is_experimental = true. It does not support
save states (retro_serialize_size returns 0).
Supported script formats: .txt, .dat, .___, .ons
needs_fullpath = true, block_extract = false.
Experimental core. No save state support (retro_serialize_size = 0).
files: []

View File

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

View File

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

View File

@@ -1,7 +1,9 @@
emulator: OpenTyrian
type: libretro
core_classification: game_engine
source: "https://github.com/trapexit/libretro-opentyrian"
profiled_date: "2026-03-18"
upstream: "https://github.com/opentyrian/opentyrian"
profiled_date: "2026-03-24"
core_version: "1.0.0.6"
display_name: "Tyrian (OpenTyrian)"
cores: [opentyrian]

View File

@@ -1,8 +1,11 @@
emulator: Opera (4DO)
type: libretro
core_classification: community_fork
core: opera_libretro
cores: [opera]
source: "https://github.com/libretro/opera-libretro"
profiled_date: "2026-03-18"
upstream: "https://sourceforge.net/projects/freedo/"
profiled_date: "2026-03-24"
core_version: "1.0.0"
display_name: "The 3DO Company - 3DO (Opera)"
systems:
@@ -27,7 +30,8 @@ notes: |
BIOS definitions: libopera/opera_bios.c BIOSES[] lines 3-136.
BIOS loading: opera_lr_opts.c opera_lr_opts_set_bios() lines 239-270.
Font loading: opera_lr_opts.c opera_lr_opts_set_font() lines 272-320.
Font loading: opera_lr_opts.c opera_lr_opts_get_font() lines 274-293,
opera_lr_opts_set_font() lines 297-328.
Core option: libretro_core_options.c opera_bios / opera_font.
files:

View File

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

View File

@@ -1,16 +1,33 @@
emulator: "ParaLLEl N64"
type: libretro
core_classification: community_fork
source: "https://github.com/libretro/parallel-n64"
upstream: "https://github.com/mupen64plus/mupen64plus-core"
profiled_date: "2026-03-24"
core_version: "2.0-rc2"
display_name: "Nintendo - Nintendo 64 (ParaLLEl N64)"
systems: [nintendo-64, nintendo-64dd]
cores: [parallel_n64, parallel_n64_debug]
# Needs full source-verified profiling. Minimal profile based on known file paths.
files:
- name: "64DD_IPL.bin"
path: "64DD_IPL.bin"
required: false
note: "64DD IPL ROM. Only needed for N64 Disk Drive games"
description: "64DD IPL ROM"
note: "Only loaded when a 64DD disk image (.ndd) is present via subsystem API. Core fails to load disk games without it."
source_ref: "libretro/libretro.c:576-623"
notes:
hle_available: true
hle_note: >
PIF boot ROM is fully HLE'd (pifbootrom/pifbootrom.c:57). CIC
challenge/response handled in software (n64_cic_nus_6105.c). No BIOS
files needed for standard cartridge games.
dd_note: >
64DD IPL ROM loaded from system_dir/64DD_IPL.bin when disk_data is
present. Loaded via fopen with no hash or size validation.
divergence_note: >
Fork of mupen64plus-core with significant divergence. Includes legacy
GPU plugins (Glide64, glN64, Rice, angrylion) and ParaLLEl-RDP/RSP.
Uses flat path 64DD_IPL.bin unlike mupen64plus-next which uses
Mupen64plus/IPL.n64 subdirectory.

View File

@@ -1,8 +1,8 @@
emulator: "parallel_n64_debug"
type: alias
alias_of: "mupen64plus"
profiled_date: "2026-03-18"
alias_of: "parallel_n64"
profiled_date: "2026-03-24"
core_version: "2.0-rc2"
display_name: "Nintendo - Nintendo 64 (ParaLLEl) (Debug)"
note: "This core uses the same BIOS/firmware as mupen64plus. See emulators/mupen64plus.yml for details."
note: "Debug build of parallel_n64. Same codebase and firmware requirements."
files: []

View File

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

View File

@@ -1,17 +1,45 @@
emulator: PCem
type: libretro
core_classification: community_fork
source: "https://github.com/libretro/libretro-pcem"
profiled_date: "2026-03-18"
core_version: "SVN"
upstream: "https://github.com/sarah-walker-pcem/pcem"
profiled_date: "2026-03-24"
core_version: "v11 (PCem-mooch, based on PCem v10.1)"
display_name: "PC (PCem)"
systems: [ibm-pc, ibm-xt, ibm-at, ibm-pcjr, ibm-ps1, tandy-1000]
cores: [pcem]
systems:
- ibm-pc
- ibm-xt
- ibm-at
- ibm-pcjr
- ibm-ps1
- tandy-1000
# PCem (PC Emulator) emulates IBM PC compatibles from 8088 through Pentium.
# All ROMs are loaded relative to the core's system directory via romfopen()
# which prepends pcempath (= RetroArch system dir) to the path.
# Each machine model requires its own BIOS ROM set under roms/<machine>/.
# Video card ROMs are separate and only needed for the selected GPU.
# The font ROM (mda.rom) is always loaded at startup.
notes:
rom_structure: >
All ROM files are loaded relative to the RetroArch system directory via
romfopen(). Machine BIOS ROMs go in roms/<machine_name>/ subdirectories.
Video card ROMs go directly in roms/ (or roms/<card>/ for some).
Which ROMs are needed depends on the selected machine model and video card.
minimum_requirement: >
At minimum, mda.rom (font) and one machine BIOS set are required.
The video card ROM for the selected GPU is also needed unless the machine
has built-in video (PCjr, Tandy, Amstrad, Acer 386SX, PS/1).
libretro_version: >
The libretro port is PCem-mooch v11, a community fork based on PCem v10.1.
Upstream PCem has evolved to v17 with many more machines and devices.
nvr_files: >
NVR files are CMOS/NVRAM templates providing pre-configured BIOS settings.
Shipped with the PCem distribution. Without them the BIOS starts with blank
CMOS (user must manually configure). The emulator also writes these back on
shutdown.
runtime_state: >
The emulator creates additional state files during operation: ATI EEPROM
(ati18800.nvr, ati28800.nvr, mach64.nvr), Intel flash (dmi.bin, escd.bin
per Pentium machine). These are emulator-generated and not included here.
info_divergence: >
The .info declares zero firmware (no firmware_count field). The core
actually loads 89+ ROM files plus NVR/EEPROM templates.
files:
# ========================================================
@@ -150,14 +178,18 @@ files:
- name: "Tandy 1000 SL/2 BIOS low"
path: "roms/tandy1000sl2/8079047.hu1"
required: false
note: "Tandy 1000 SL/2 BIOS low chip. Paired with 8079048.hu2."
source_ref: "src/mem.c:180"
note: >
Tandy 1000 SL/2 BIOS low chip. Paired with 8079048.hu2.
Also loaded by tandy_rom.c for full bank-switched ROM (512 KB).
source_ref: "src/mem.c:180, src/tandy_rom.c:58"
- name: "Tandy 1000 SL/2 BIOS high"
path: "roms/tandy1000sl2/8079048.hu2"
required: false
note: "Tandy 1000 SL/2 BIOS high chip. Paired with 8079047.hu1."
source_ref: "src/mem.c:181"
note: >
Tandy 1000 SL/2 BIOS high chip. Paired with 8079047.hu1.
Also loaded by tandy_rom.c for full bank-switched ROM (512 KB).
source_ref: "src/mem.c:181, src/tandy_rom.c:59"
# ========================================================
# MACHINE BIOS ROMs - Amstrad / Sinclair
@@ -360,7 +392,7 @@ files:
- name: "Acer 386SX BIOS"
path: "roms/acer386/acer386.bin"
required: false
note: "Acer 386SX/25N BIOS (64 KB). Also requires oti067.bin video ROM."
note: "Acer 386SX/25N BIOS (64 KB)."
source_ref: "src/mem.c:353-356"
- name: "Acer 386SX OTI-067 video BIOS"
@@ -634,8 +666,8 @@ files:
path: "roms/mach64gx/bios.bin"
required: false
note: >
ATI Graphics Pro Turbo (Mach64 GX) video BIOS. Also used by the
Cirrus Logic CL-GD5429 init path (likely a copy/paste in the source).
ATI Graphics Pro Turbo (Mach64 GX) video BIOS. Also loaded by the
Cirrus Logic init path (copy/paste in source).
source_ref: "src/vid_ati_mach64.c:2341, src/vid_cirrus.c:2324"
- name: "Cirrus Logic CL-GD5429 BIOS"
@@ -696,16 +728,224 @@ files:
note: "NE2000 ISA Ethernet boot ROM (64 KB)."
source_ref: "src/ne2000.c:1701"
notes:
rom_structure: >
All ROM files are loaded relative to the RetroArch system directory.
Machine BIOS ROMs go in roms/<machine_name>/ subdirectories.
Video card ROMs go directly in roms/ (or roms/<card>/ for some).
Which ROMs are needed depends on the selected machine model and video card.
minimum_requirement: >
At minimum, the mda.rom font and one machine BIOS set are required.
The video card ROM for the selected GPU is also needed unless the machine
has built-in video (PCjr, Tandy, Amstrad, Acer 386SX, PS/1).
libretro_version: >
Based on PCem v10.1. The libretro port is quite old and may not match
current standalone PCem/86Box ROM paths.
# ========================================================
# NVR/CMOS TEMPLATES (shipped with PCem distribution)
# Pre-configured BIOS settings. The emulator loads these on
# startup and writes back on shutdown. Without them the BIOS
# starts with blank CMOS settings.
# ========================================================
- name: "NVR: Amstrad PC1512"
path: "pc1512.nvr"
required: false
note: "CMOS/NVRAM template for Amstrad PC1512."
source_ref: "src/nvr.c:390"
- name: "NVR: Amstrad PC1640"
path: "pc1640.nvr"
required: false
note: "CMOS/NVRAM template for Amstrad PC1640."
source_ref: "src/nvr.c:391"
- name: "NVR: Sinclair PC200"
path: "pc200.nvr"
required: false
note: "CMOS/NVRAM template for Sinclair PC200."
source_ref: "src/nvr.c:392"
- name: "NVR: Amstrad PC2086"
path: "pc2086.nvr"
required: false
note: "CMOS/NVRAM template for Amstrad PC2086."
source_ref: "src/nvr.c:393"
- name: "NVR: Amstrad PC3086"
path: "pc3086.nvr"
required: false
note: "CMOS/NVRAM template for Amstrad PC3086."
source_ref: "src/nvr.c:394"
- name: "NVR: IBM AT"
path: "at.nvr"
required: false
note: "CMOS/NVRAM template for IBM AT."
source_ref: "src/nvr.c:395"
- name: "NVR: IBM PS/1 model 2011"
path: "ibmps1_2011.nvr"
required: false
note: "CMOS/NVRAM template for IBM PS/1 model 2011."
source_ref: "src/nvr.c:396"
- name: "NVR: Commodore PC 30 III"
path: "cmdpc30.nvr"
required: false
note: "CMOS/NVRAM template for Commodore PC 30 III."
source_ref: "src/nvr.c:397"
- name: "NVR: AMI 286"
path: "ami286.nvr"
required: false
note: "CMOS/NVRAM template for AMI 286 clone."
source_ref: "src/nvr.c:398"
- name: "NVR: Dell System 200"
path: "dell200.nvr"
required: false
note: "CMOS/NVRAM template for Dell System 200."
source_ref: "src/nvr.c:399"
- name: "NVR: IBM AT 386"
path: "at386.nvr"
required: false
note: "CMOS/NVRAM template for IBM AT 386."
source_ref: "src/nvr.c:400"
- name: "NVR: Compaq Deskpro 386"
path: "deskpro386.nvr"
required: false
note: "CMOS/NVRAM template for Compaq Deskpro 386."
source_ref: "src/nvr.c:401"
- name: "NVR: Acer 386SX"
path: "acer386.nvr"
required: false
note: "CMOS/NVRAM template for Acer 386SX/25N."
source_ref: "src/nvr.c:402"
- name: "NVR: Amstrad MegaPC"
path: "megapc.nvr"
required: false
note: "CMOS/NVRAM template for Amstrad MegaPC."
source_ref: "src/nvr.c:403"
- name: "NVR: AMI 386"
path: "ami386.nvr"
required: false
note: "CMOS/NVRAM template for AMI 386 clone."
source_ref: "src/nvr.c:404"
- name: "NVR: AMI 486"
path: "ami486.nvr"
required: false
note: "CMOS/NVRAM template for AMI 486 clone."
source_ref: "src/nvr.c:405"
- name: "NVR: AMI WinBIOS 486"
path: "win486.nvr"
required: false
note: "CMOS/NVRAM template for AMI WinBIOS 486."
source_ref: "src/nvr.c:406"
- name: "NVR: HOT-433 PCI 486"
path: "hot-433.nvr"
required: false
note: "CMOS/NVRAM template for HOT-433 PCI 486."
source_ref: "src/nvr.c:407"
- name: "NVR: Award SiS 496"
path: "sis496.nvr"
required: false
note: "CMOS/NVRAM template for Award SiS 496/497."
source_ref: "src/nvr.c:408"
- name: "NVR: Award 430VX"
path: "430vx.nvr"
required: false
note: "CMOS/NVRAM template for Award 430VX PCI."
source_ref: "src/nvr.c:409"
- name: "NVR: Intel Premiere/PCI (Batman)"
path: "revenge.nvr"
required: false
note: "CMOS/NVRAM template for Intel Premiere/PCI."
source_ref: "src/nvr.c:410"
- name: "NVR: Intel Advanced/EV (Endeavor)"
path: "endeavor.nvr"
required: false
note: "CMOS/NVRAM template for Intel Advanced/EV."
source_ref: "src/nvr.c:411"
- name: "NVR: Phoenix 386"
path: "px386.nvr"
required: false
note: "CMOS/NVRAM template for Phoenix 386 clone."
source_ref: "src/nvr.c:412"
- name: "NVR: DTK 386"
path: "dtk386.nvr"
required: false
note: "CMOS/NVRAM template for DTK 386SX clone."
source_ref: "src/nvr.c:413"
- name: "NVR: DTK 486"
path: "dtk486.nvr"
required: false
note: "CMOS/NVRAM template for DTK 486."
source_ref: "src/nvr.c:414"
- name: "NVR: Rise R418"
path: "r418.nvr"
required: false
note: "CMOS/NVRAM template for Rise Computer R418."
source_ref: "src/nvr.c:415"
- name: "NVR: Intel Premiere/PCI II (Plato)"
path: "plato.nvr"
required: false
note: "CMOS/NVRAM template for Intel Premiere/PCI II."
source_ref: "src/nvr.c:416"
- name: "NVR: PC Partner MB500N"
path: "mb500n.nvr"
required: false
note: "CMOS/NVRAM template for PC Partner MB500N."
source_ref: "src/nvr.c:417"
- name: "NVR: Acer M3A"
path: "acerm3a.nvr"
required: false
note: "CMOS/NVRAM template for Acer M3A."
source_ref: "src/nvr.c:418"
- name: "NVR: Acer V35N"
path: "acerv35n.nvr"
required: false
note: "CMOS/NVRAM template for Acer V35N."
source_ref: "src/nvr.c:419"
- name: "NVR: ASUS P/I-P55T2P4"
path: "p55t2p4.nvr"
required: false
note: "CMOS/NVRAM template for ASUS P/I-P55T2P4."
source_ref: "src/nvr.c:420"
- name: "NVR: Epox P55-VA"
path: "p55va.nvr"
required: false
note: "CMOS/NVRAM template for Epox P55-VA."
source_ref: "src/nvr.c:421"
# ========================================================
# EEPROM TEMPLATES (shipped with upstream PCem)
# Persistent settings for Tandy EEPROM and AdLib Gold.
# Loaded on startup, saved on shutdown. Without them the
# hardware starts with blank/default configuration.
# ========================================================
- name: "Tandy 1000 HX EEPROM"
path: "tandy1000hx.bin"
required: false
note: "Tandy 1000 HX EEPROM state (128 bytes). Initializes to zeros if absent."
source_ref: "src/tandy_eeprom.c:128"
- name: "Tandy 1000 SL/2 EEPROM"
path: "tandy1000sl2.bin"
required: false
note: "Tandy 1000 SL/2 EEPROM state (128 bytes). Initializes to zeros if absent."
source_ref: "src/tandy_eeprom.c:131"
- name: "AdLib Gold EEPROM"
path: "nvr/adgold.bin"
required: false
note: "AdLib Gold sound card EEPROM state (24 bytes). Initializes to defaults if absent."
source_ref: "src/sound_adlibgold.c:789"

View File

@@ -1,8 +1,46 @@
emulator: "pcsx1"
type: alias
alias_of: "pcsx_rearmed"
profiled_date: "2026-03-18"
core_version: "r21"
emulator: PCSX1
type: libretro
core_classification: community_fork
source: "https://github.com/libretro/pcsx1-libretro"
upstream: "https://github.com/notaz/pcsx_rearmed"
profiled_date: "2026-03-24"
core_version: "r22"
display_name: "Sony - PlayStation (PCSX1)"
note: "This core uses the same BIOS/firmware as pcsx_rearmed. See emulators/pcsx_rearmed.yml for details."
files: []
cores: [pcsx1]
systems: [sony-playstation]
notes: >
Never-completed fork of PCSX-ReARMed targeting general PC hardware. Experimental, abandoned since 2018.
HLE BIOS built-in: if no BIOS file found, core warns and falls back to HLE automatically.
BIOS search order: scph1001.bin, scph5501.bin, scph7001.bin by exact filename, then scans system
directory for any file matching "scph*" (case-insensitive) with size == 512 KB.
No region detection, no hash validation. First valid BIOS found is used regardless of region.
.info declares scph5500/5501/5502 but code explicitly searches scph1001/5501/7001 first.
files:
- name: "scph1001.bin"
description: "SCPH-1001 (v2.2 12-04-95 A)"
required: false
hle_fallback: true
size: 524288
validation: [size]
source_ref: "frontend/libretro.c:1229 (bios[0]), :1181 (size check)"
- name: "scph5501.bin"
description: "SCPH-5501 (v3.0 11-18-96 A)"
required: false
hle_fallback: true
size: 524288
validation: [size]
source_ref: "frontend/libretro.c:1229 (bios[1]), :1181 (size check)"
- name: "scph7001.bin"
description: "SCPH-7001 (v4.1 12-16-97 A)"
required: false
hle_fallback: true
size: 524288
validation: [size]
source_ref: "frontend/libretro.c:1229 (bios[2]), :1181 (size check)"
# Dynamic scan fallback: after the 3 named files, find_any_bios() scans the
# system directory for any file starting with "scph" (case-insensitive) that
# is exactly 512 KB. This covers scph5500.bin, scph5502.bin, scph3000.bin, etc.

View File

@@ -1,13 +1,13 @@
emulator: PCSX-ReARMed
type: libretro
core_classification: embedded_hle
source: "https://github.com/libretro/pcsx_rearmed"
profiled_date: "2026-03-18"
core_version: "r24"
upstream: "https://github.com/notaz/pcsx_rearmed"
profiled_date: "2026-03-24"
core_version: "r25"
display_name: "Sony - PlayStation (PCSX ReARMed)"
cores: [pcsx_rearmed, pcsx_rearmed_neon, pcsx_rearmed_interpreter]
systems: [sony-playstation]
bios_size: 524288 # 512 KB (0x80000); also accepts 4 MB psxonpsp combo (reads first 512 KB)
verification: crc32
notes: >
HLE BIOS built-in: core option pcsx_rearmed_bios = "HLE" bypasses real BIOS entirely.
Default is "auto" which searches system dir for listed filenames, then scans all files.
@@ -17,7 +17,6 @@ notes: >
Files named "unirom" (case-insensitive) are explicitly skipped.
Region fallback: if the matching-region BIOS is missing, any available region BIOS is used over HLE.
Three region slots: US (index 0), JP (index 1), EU (index 2) stored in Config.Bios[].
All firmware is optional per libretro-core-info; HLE works but real BIOS improves compatibility.
files:
# -- Region: Japan (PSX_REGION_JP = 1) --
@@ -25,10 +24,11 @@ files:
- name: "scph5500.bin"
description: "SCPH-5500 (v3.0 09-09-96 J)"
region: "NTSC-J"
size: 524288
required: false
hle_fallback: true
md5: "8dd7d5296a650fac7319bce665a6a53c"
source_ref: "frontend/libretro.c:3710 (listed_bios[0])"
validation: [size]
source_ref: "frontend/libretro.c:3721-3724 (listed_bios[0])"
notes: "Preferred JP BIOS, searched first by exact filename."
# -- Region: North America (PSX_REGION_US = 0) --
@@ -36,10 +36,11 @@ files:
- name: "scph5501.bin"
description: "SCPH-5501 (v3.0 11-18-96 A)"
region: "NTSC-U"
size: 524288
required: false
hle_fallback: true
md5: "490f666e1afb15b7362b406ed1cea246"
source_ref: "frontend/libretro.c:3710 (listed_bios[1])"
validation: [size]
source_ref: "frontend/libretro.c:3721-3724 (listed_bios[1])"
notes: "Preferred US BIOS, searched first by exact filename."
# -- Region: Europe (PSX_REGION_EU = 2) --
@@ -47,48 +48,52 @@ files:
- name: "scph5502.bin"
description: "SCPH-5502 (v3.0 01-06-97 E)"
region: "PAL"
size: 524288
required: false
hle_fallback: true
md5: "32736f17079d0b2b7024407c39bd3050"
source_ref: "frontend/libretro.c:3710 (listed_bios[2])"
validation: [size]
source_ref: "frontend/libretro.c:3721-3724 (listed_bios[2])"
notes: "Preferred EU BIOS, searched first by exact filename."
# -- Fallback BIOS filenames (searched in order after scph550x) --
- name: "psxonpsp660.bin"
description: "PSP embedded PS1 BIOS (region-free, 512 KB or 4 MB accepted)"
description: "PSP embedded PS1 BIOS (region detected at runtime)"
region: "Auto"
size: 524288
required: false
hle_fallback: true
md5: "c53ca5908936d412331790f4426c6c33"
source_ref: "frontend/libretro.c:3711 (listed_bios[3])"
notes: "Region detected from content at runtime. 4 MB combo image accepted but only first 512 KB read."
validation: [size]
source_ref: "frontend/libretro.c:3721-3724 (listed_bios[3])"
notes: "Region detected from content at runtime. Must be 512 KB in libretro context."
- name: "scph101.bin"
description: "SCPH-101 (v4.4 03-24-00 A) - PSone US"
region: "NTSC-U"
size: 524288
required: false
hle_fallback: true
md5: "6e3735ff4c7dc899ee98981c18c3666d"
source_ref: "frontend/libretro.c:3711 (listed_bios[4])"
notes: "PSone slim model. Searched by filename after scph550x and psxonpsp660."
validation: [size]
source_ref: "frontend/libretro.c:3721-3724 (listed_bios[4])"
notes: "PSone slim model."
- name: "scph7001.bin"
description: "SCPH-7001 (v4.1 12-16-97 A)"
region: "NTSC-U"
size: 524288
required: false
hle_fallback: true
md5: "1e68c231d0896b7eadcad1d7d8e76129"
source_ref: "frontend/libretro.c:3711 (listed_bios[5])"
notes: "Searched by filename after psxonpsp660 and scph101."
validation: [size]
source_ref: "frontend/libretro.c:3721-3724 (listed_bios[5])"
- name: "scph1001.bin"
description: "SCPH-1001 (v2.2 12-04-95 A)"
region: "NTSC-U"
size: 524288
required: false
hle_fallback: true
md5: "924e392ed05558ffdb115408c263dccf"
source_ref: "frontend/libretro.c:3711 (listed_bios[6])"
validation: [size]
source_ref: "frontend/libretro.c:3721-3724 (listed_bios[6])"
notes: "Original US model. Last in the explicit filename search list."
# -- Dynamic scan fallback --

View File

@@ -1,9 +1,12 @@
emulator: PicoDrive
type: libretro
core_classification: official_port
source: "https://github.com/libretro/picodrive"
profiled_date: "2026-03-18"
upstream: "https://github.com/irixxxx/picodrive"
profiled_date: "2026-03-24"
core_version: "1.99"
display_name: "Sega - MS/GG/MD/CD/32X (PicoDrive)"
cores: [picodrive]
systems:
- sega-megadrive
- sega-genesis
@@ -13,33 +16,48 @@ systems:
- sega-mastersystem
- sega-gamegear
- sega-sg1000
- sega-sc3000
- sega-pico
notes: |
PicoDrive is a fast Mega Drive / Genesis emulator with Mega CD, 32X, Master
System, Game Gear, SG-1000 and Sega Pico support.
System, Game Gear, SG-1000, SC-3000 and Sega Pico support. The libretro port
is part of the PicoDrive source tree (platform/libretro/).
Mega CD / Sega CD games require a region-matching BIOS file. The core searches
the system directory for each name in order, trying .bin then .zip extension,
and uses the first file found. If no BIOS is found, CD games fail to load with
PM_BAD_CD_NO_BIOS. MSU-MD games can run without BIOS.
BIOS filename search order (platform/libretro/libretro.c:1265-1329):
BIOS filename search order (platform/libretro/libretro.c:1264-1328):
US: us_scd2_9306, SegaCDBIOS9303, us_scd1_9210, bios_CD_U
EU: eu_mcd2_9306, eu_mcd2_9303, eu_mcd1_9210, bios_CD_E
JP: jp_mcd2_921222, jp_mcd1_9112, jp_mcd1_9111, bios_CD_J
32X BIOS files (m68k, master SH2, slave SH2) are fully optional. The core
has built-in HLE that generates replacement code at startup when the external
BIOS pointers are NULL (pico/32x/memory.c:2200 get_bios(), pico/32x/32x.c:172).
The libretro frontend does not expose any 32X BIOS loading path. Only the
standalone platform code references 32X_M_BIOS.BIN / 32X_S_BIOS.BIN, and
that code is currently disabled (#if 0 in platform/common/emu.c:1529).
32X BIOS files (m68k, master SH2, slave SH2) are fully optional. The engine
has built-in HLE that generates replacement code at startup when the BIOS
pointers are NULL (pico/32x/memory.c:2199 get_bios(), pico/32x/32x.c:172).
No current frontend (libretro or standalone) loads these files; the standalone
loading code is disabled (#if 0 in platform/common/emu.c:1529).
Master System, Game Gear, SG-1000: no BIOS file loaded. The core initializes
VDP registers and RAM to simulate post-BIOS state (pico/sms.c:1080-1096).
carthw.cfg is a cartridge hardware database loaded from the system directory.
A built-in version is compiled into the core (pico/carthw_cfg.c). The external
file supplements or overrides game-specific hardware detection (SVP, EEPROM
types, special mappers, copy protection).
Master System, Game Gear, SG-1000, SC-3000: no BIOS file loaded. The core
initializes VDP registers and RAM to simulate post-BIOS state
(pico/sms.c:1080-1097).
files:
# -------------------------------------------------------
# Cartridge hardware database
# -------------------------------------------------------
- name: "carthw.cfg"
required: false
note: "Cartridge hardware database for special mapper and EEPROM detection. Built-in fallback compiled into the core."
source_ref: "platform/libretro/libretro.c:1619, pico/cart.c:1020-1022"
# -------------------------------------------------------
# Mega CD / Sega CD - US region
# -------------------------------------------------------
@@ -48,28 +66,28 @@ files:
required: true
size: 131072 # 128 KB (0x20000)
note: "US Sega CD Model 2 BIOS (September 1993). First in US search order."
source_ref: "platform/libretro/libretro.c:1266"
source_ref: "platform/libretro/libretro.c:1265"
- name: "SegaCDBIOS9303.bin"
system: sega-segacd
required: false
size: 131072
note: "US Sega CD BIOS (March 1993). Second in US search order."
source_ref: "platform/libretro/libretro.c:1266"
source_ref: "platform/libretro/libretro.c:1265"
- name: "us_scd1_9210.bin"
system: sega-segacd
required: false
size: 131072
note: "US Sega CD Model 1 BIOS (October 1992). Third in US search order."
source_ref: "platform/libretro/libretro.c:1266"
source_ref: "platform/libretro/libretro.c:1265"
- name: "bios_CD_U.bin"
system: sega-segacd
required: false
size: 131072
note: "US Sega CD BIOS (generic name). Last in US search order."
source_ref: "platform/libretro/libretro.c:1266"
source_ref: "platform/libretro/libretro.c:1265"
# -------------------------------------------------------
# Mega CD / Sega CD - EU region
@@ -79,28 +97,28 @@ files:
required: true
size: 131072
note: "EU Mega CD Model 2 BIOS (June 1993). First in EU search order."
source_ref: "platform/libretro/libretro.c:1269"
source_ref: "platform/libretro/libretro.c:1268"
- name: "eu_mcd2_9303.bin"
system: sega-megacd
required: false
size: 131072
note: "EU Mega CD Model 2 BIOS (March 1993). Second in EU search order."
source_ref: "platform/libretro/libretro.c:1269"
source_ref: "platform/libretro/libretro.c:1268"
- name: "eu_mcd1_9210.bin"
system: sega-megacd
required: false
size: 131072
note: "EU Mega CD Model 1 BIOS (October 1992). Third in EU search order."
source_ref: "platform/libretro/libretro.c:1269"
source_ref: "platform/libretro/libretro.c:1268"
- name: "bios_CD_E.bin"
system: sega-megacd
required: false
size: 131072
note: "EU Mega CD BIOS (generic name). Last in EU search order."
source_ref: "platform/libretro/libretro.c:1269"
source_ref: "platform/libretro/libretro.c:1268"
# -------------------------------------------------------
# Mega CD / Sega CD - JP region
@@ -110,31 +128,31 @@ files:
required: true
size: 131072
note: "JP Mega CD Model 2 BIOS (December 1992). First in JP search order."
source_ref: "platform/libretro/libretro.c:1272"
source_ref: "platform/libretro/libretro.c:1271"
- name: "jp_mcd1_9112.bin"
system: sega-megacd
required: false
size: 131072
note: "JP Mega CD Model 1 BIOS (December 1991). Second in JP search order."
source_ref: "platform/libretro/libretro.c:1272"
source_ref: "platform/libretro/libretro.c:1271"
- name: "jp_mcd1_9111.bin"
system: sega-megacd
required: false
size: 131072
note: "JP Mega CD Model 1 BIOS (November 1991). Third in JP search order."
source_ref: "platform/libretro/libretro.c:1272"
source_ref: "platform/libretro/libretro.c:1271"
- name: "bios_CD_J.bin"
system: sega-megacd
required: false
size: 131072
note: "JP Mega CD BIOS (generic name). Last in JP search order."
source_ref: "platform/libretro/libretro.c:1272"
source_ref: "platform/libretro/libretro.c:1271"
# -------------------------------------------------------
# Sega 32X - HLE available, not loaded by libretro frontend
# Sega 32X - HLE available, no frontend loads these
# -------------------------------------------------------
- name: "32X_G_BIOS.BIN"
system: sega-32x
@@ -142,7 +160,7 @@ files:
hle_fallback: true
size: 256 # 0x100
note: "32X 68K (Genesis-side) BIOS. HLE replacement generated when NULL."
source_ref: "pico/32x/memory.c:2207-2243"
source_ref: "pico/32x/memory.c:2206-2243"
- name: "32X_M_BIOS.BIN"
system: sega-32x
@@ -150,7 +168,7 @@ files:
hle_fallback: true
size: 2048 # 0x800
note: "32X Master SH2 BIOS. HLE replacement generated when NULL."
source_ref: "pico/32x/memory.c:2250-2277"
source_ref: "pico/32x/memory.c:2249-2276"
- name: "32X_S_BIOS.BIN"
system: sega-32x
@@ -158,7 +176,7 @@ files:
hle_fallback: true
size: 1024 # 0x400
note: "32X Slave SH2 BIOS. HLE replacement generated when NULL."
source_ref: "pico/32x/memory.c:2280-2298"
source_ref: "pico/32x/memory.c:2279-2297"
platform_details:
megacd:
@@ -166,16 +184,16 @@ platform_details:
hle_available: false
region_specific: true
extensions_tried: [".bin", ".zip"]
source_ref: "pico/pico_int.h:559, platform/libretro/libretro.c:1310-1318"
source_ref: "pico/pico_int.h:559, platform/libretro/libretro.c:1309-1317"
32x:
m68k_bios_size: 256 # 0x100
master_sh2_bios_size: 2048 # 0x800
slave_sh2_bios_size: 1024 # 0x400
hle_available: true
source_ref: "pico/pico.h:53-55, pico/pico_int.h:679-693"
source_ref: "pico/pico.h:54-55, pico/pico_int.h:679-693"
sms:
hle_available: true
note: "No BIOS file loaded. VDP/RAM initialized to post-BIOS state."
source_ref: "pico/sms.c:1080-1096"
source_ref: "pico/sms.c:1080-1097"

View File

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

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("/"))
h = hashlib.md5()
for name in names:
info = zf.getinfo(name)
if info.file_size > 512 * 1024 * 1024:
continue # skip oversized entries
h.update(zf.read(name))
result = h.hexdigest()
_md5_composite_cache[key] = result
return result
def parse_md5_list(raw: str) -> list[str]:
"""Parse comma-separated MD5 string into normalized lowercase list."""
return [m.strip().lower() for m in raw.split(",") if m.strip()] if raw else []
def load_platform_config(platform_name: str, platforms_dir: str = "platforms") -> dict:
"""Load a platform config with inheritance and shared group resolution.
@@ -162,6 +170,7 @@ def resolve_local_file(
db: dict,
zip_contents: dict | None = None,
dest_hint: str = "",
_depth: int = 0,
) -> tuple[str | None, str]:
"""Resolve a BIOS file to its local path using database.json.
@@ -293,13 +302,16 @@ def resolve_local_file(
return path, "zip_exact"
# MAME clone fallback: if a file was deduped, resolve via canonical
clone_map = _get_mame_clone_map()
canonical = clone_map.get(name)
if canonical and canonical != name:
canonical_entry = {"name": canonical}
result = resolve_local_file(canonical_entry, db, zip_contents, dest_hint)
if result[0]:
return result[0], "mame_clone"
if _depth < 3:
clone_map = _get_mame_clone_map()
canonical = clone_map.get(name)
if canonical and canonical != name:
canonical_entry = {"name": canonical}
result = resolve_local_file(
canonical_entry, db, zip_contents, dest_hint, _depth=_depth + 1,
)
if result[0]:
return result[0], "mame_clone"
return None, "not_found"
@@ -333,6 +345,9 @@ def check_inside_zip(container: str, file_name: str, expected_md5: str) -> str:
with zipfile.ZipFile(container) as archive:
for fname in archive.namelist():
if fname.casefold() == file_name.casefold():
info = archive.getinfo(fname)
if info.file_size > 512 * 1024 * 1024:
return "error"
if expected_md5 == "":
return "ok"
with archive.open(fname) as entry:
@@ -365,10 +380,16 @@ def build_zip_contents_index(db: dict, max_entry_size: int = 512 * 1024 * 1024)
return index
_emulator_profiles_cache: dict[tuple[str, bool], dict[str, dict]] = {}
def load_emulator_profiles(
emulators_dir: str, skip_aliases: bool = True,
) -> dict[str, dict]:
"""Load all emulator YAML profiles from a directory."""
"""Load all emulator YAML profiles from a directory (cached)."""
cache_key = (os.path.realpath(emulators_dir), skip_aliases)
if cache_key in _emulator_profiles_cache:
return _emulator_profiles_cache[cache_key]
try:
import yaml
except ImportError:
@@ -385,6 +406,7 @@ def load_emulator_profiles(
if skip_aliases and profile.get("type") == "alias":
continue
profiles[f.stem] = profile
_emulator_profiles_cache[cache_key] = profiles
return profiles
@@ -461,6 +483,192 @@ def resolve_platform_cores(
}
def _parse_validation(validation: list | dict | None) -> list[str]:
"""Extract the validation check list from a file's validation field.
Handles both simple list and divergent (core/upstream) dict forms.
For dicts, uses the ``core`` key since RetroArch users run the core.
"""
if validation is None:
return []
if isinstance(validation, list):
return validation
if isinstance(validation, dict):
return validation.get("core", [])
return []
# Validation types that require console-specific cryptographic keys.
# verify.py cannot reproduce these — size checks still apply if combined.
_CRYPTO_CHECKS = frozenset({"signature", "crypto"})
# All reproducible validation types.
_HASH_CHECKS = frozenset({"crc32", "md5", "sha1", "adler32"})
def _build_validation_index(profiles: dict) -> dict[str, dict]:
"""Build per-filename validation rules from emulator profiles.
Returns {filename: {"checks": [str], "size": int|None, "min_size": int|None,
"max_size": int|None, "crc32": str|None, "md5": str|None, "sha1": str|None,
"adler32": str|None, "crypto_only": [str]}}.
``crypto_only`` lists validation types we cannot reproduce (signature, crypto)
so callers can report them as non-verifiable rather than silently skipping.
When multiple emulators reference the same file, merges checks (union).
Raises ValueError if two profiles declare conflicting values.
"""
index: dict[str, dict] = {}
sources: dict[str, dict[str, str]] = {}
for emu_name, profile in profiles.items():
if profile.get("type") in ("launcher", "alias"):
continue
for f in profile.get("files", []):
fname = f.get("name", "")
if not fname:
continue
checks = _parse_validation(f.get("validation"))
if not checks:
continue
if fname not in index:
index[fname] = {
"checks": set(), "sizes": set(),
"min_size": None, "max_size": None,
"crc32": set(), "md5": set(), "sha1": set(), "sha256": set(),
"adler32": set(), "crypto_only": set(),
}
sources[fname] = {}
index[fname]["checks"].update(checks)
# Track non-reproducible crypto checks
index[fname]["crypto_only"].update(
c for c in checks if c in _CRYPTO_CHECKS
)
# Size checks
if "size" in checks:
if f.get("size") is not None:
index[fname]["sizes"].add(f["size"])
if f.get("min_size") is not None:
cur = index[fname]["min_size"]
index[fname]["min_size"] = min(cur, f["min_size"]) if cur is not None else f["min_size"]
if f.get("max_size") is not None:
cur = index[fname]["max_size"]
index[fname]["max_size"] = max(cur, f["max_size"]) if cur is not None else f["max_size"]
# Hash checks — collect all accepted hashes as sets (multiple valid
# versions of the same file, e.g. MT-32 ROM versions)
if "crc32" in checks and f.get("crc32"):
norm = f["crc32"].lower()
if norm.startswith("0x"):
norm = norm[2:]
index[fname]["crc32"].add(norm)
for hash_type in ("md5", "sha1", "sha256"):
if hash_type in checks and f.get(hash_type):
index[fname][hash_type].add(f[hash_type].lower())
# Adler32 — stored as known_hash_adler32 field (not in validation: list
# for Dolphin, but support it in both forms for future profiles)
adler_val = f.get("known_hash_adler32") or f.get("adler32")
if adler_val:
norm = adler_val.lower()
if norm.startswith("0x"):
norm = norm[2:]
index[fname]["adler32"].add(norm)
# Convert sets to sorted tuples/lists for determinism
for v in index.values():
v["checks"] = sorted(v["checks"])
v["crypto_only"] = sorted(v["crypto_only"])
# Keep hash sets as frozensets for O(1) lookup in check_file_validation
return index
def check_file_validation(
local_path: str, filename: str, validation_index: dict[str, dict],
bios_dir: str = "bios",
) -> str | None:
"""Check emulator-level validation on a resolved file.
Supports: size (exact/min/max), crc32, md5, sha1, adler32,
signature (RSA-2048 PKCS1v15 SHA256), crypto (AES-128-CBC + SHA256).
Returns None if all checks pass or no validation applies.
Returns a reason string if a check fails.
"""
entry = validation_index.get(filename)
if not entry:
return None
checks = entry["checks"]
# Size checks — sizes is a set of accepted values
if "size" in checks:
actual_size = os.path.getsize(local_path)
if entry["sizes"] and actual_size not in entry["sizes"]:
expected = ",".join(str(s) for s in sorted(entry["sizes"]))
return f"size mismatch: got {actual_size}, accepted [{expected}]"
if entry["min_size"] is not None and actual_size < entry["min_size"]:
return f"size too small: min {entry['min_size']}, got {actual_size}"
if entry["max_size"] is not None and actual_size > entry["max_size"]:
return f"size too large: max {entry['max_size']}, got {actual_size}"
# Hash checks — compute once, reuse for all hash types.
# Each hash field is a set of accepted values (multiple valid ROM versions).
need_hashes = (
any(h in checks and entry.get(h) for h in ("crc32", "md5", "sha1", "sha256"))
or entry.get("adler32")
)
if need_hashes:
hashes = compute_hashes(local_path)
if "crc32" in checks and entry["crc32"]:
if hashes["crc32"].lower() not in entry["crc32"]:
expected = ",".join(sorted(entry["crc32"]))
return f"crc32 mismatch: got {hashes['crc32']}, accepted [{expected}]"
if "md5" in checks and entry["md5"]:
if hashes["md5"].lower() not in entry["md5"]:
expected = ",".join(sorted(entry["md5"]))
return f"md5 mismatch: got {hashes['md5']}, accepted [{expected}]"
if "sha1" in checks and entry["sha1"]:
if hashes["sha1"].lower() not in entry["sha1"]:
expected = ",".join(sorted(entry["sha1"]))
return f"sha1 mismatch: got {hashes['sha1']}, accepted [{expected}]"
if "sha256" in checks and entry["sha256"]:
if hashes["sha256"].lower() not in entry["sha256"]:
expected = ",".join(sorted(entry["sha256"]))
return f"sha256 mismatch: got {hashes['sha256']}, accepted [{expected}]"
if entry["adler32"]:
if hashes["adler32"].lower() not in entry["adler32"]:
expected = ",".join(sorted(entry["adler32"]))
return f"adler32 mismatch: got 0x{hashes['adler32']}, accepted [{expected}]"
# Signature/crypto checks (3DS RSA, AES)
if entry["crypto_only"]:
from crypto_verify import check_crypto_validation
crypto_reason = check_crypto_validation(local_path, filename, bios_dir)
if crypto_reason:
return crypto_reason
return None
def validate_cli_modes(args, mode_attrs: list[str]) -> None:
"""Validate mutual exclusion of CLI mode arguments."""
modes = sum(1 for attr in mode_attrs if getattr(args, attr, None))
if modes == 0:
raise SystemExit(f"Specify one of: --{' --'.join(mode_attrs)}")
if modes > 1:
raise SystemExit(f"Options are mutually exclusive: --{' --'.join(mode_attrs)}")
def filter_files_by_mode(files: list[dict], standalone: bool) -> list[dict]:
"""Filter file entries by libretro/standalone mode."""
result = []
for f in files:
fmode = f.get("mode", "")
if standalone and fmode == "libretro":
continue
if not standalone and fmode == "standalone":
continue
result.append(f)
return result
def safe_extract_zip(zip_path: str, dest_dir: str) -> None:
"""Extract a ZIP file safely, preventing zip-slip path traversal."""
dest = os.path.realpath(dest_dir)
@@ -470,3 +678,31 @@ def safe_extract_zip(zip_path: str, dest_dir: str) -> None:
if not member_path.startswith(dest + os.sep) and member_path != dest:
raise ValueError(f"Zip slip detected: {member.filename}")
zf.extract(member, dest)
def list_emulator_profiles(emulators_dir: str, skip_aliases: bool = True) -> None:
"""Print available emulator profiles."""
profiles = load_emulator_profiles(emulators_dir, skip_aliases=False)
for name in sorted(profiles):
p = profiles[name]
if p.get("type") in ("alias", "test"):
continue
display = p.get("emulator", name)
ptype = p.get("type", "libretro")
systems = ", ".join(p.get("systems", [])[:3])
more = "..." if len(p.get("systems", [])) > 3 else ""
print(f" {name:30s} {display:40s} [{ptype}] {systems}{more}")
def list_system_ids(emulators_dir: str) -> None:
"""Print available system IDs with emulator count."""
profiles = load_emulator_profiles(emulators_dir)
system_emus: dict[str, list[str]] = {}
for name, p in profiles.items():
if p.get("type") in ("alias", "test", "launcher"):
continue
for sys_id in p.get("systems", []):
system_emus.setdefault(sys_id, []).append(name)
for sys_id in sorted(system_emus):
count = len(system_emus[sys_id])
print(f" {sys_id:35s} ({count} emulator{'s' if count > 1 else ''})")

View File

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

View File

@@ -25,10 +25,11 @@ from pathlib import Path
sys.path.insert(0, os.path.dirname(__file__))
from common import (
build_zip_contents_index, check_inside_zip, compute_hashes,
group_identical_platforms, load_database, load_data_dir_registry,
load_emulator_profiles, load_platform_config, md5_composite,
resolve_local_file,
_build_validation_index, build_zip_contents_index, check_file_validation,
check_inside_zip, compute_hashes, filter_files_by_mode,
group_identical_platforms, list_emulator_profiles, list_system_ids,
load_database, load_data_dir_registry, load_emulator_profiles,
load_platform_config, md5_composite, resolve_local_file,
)
from deterministic_zip import rebuild_zip_deterministic
@@ -256,7 +257,6 @@ def generate_pack(
file_reasons: dict[str, str] = {}
# Build emulator-level validation index (same as verify.py)
from verify import _build_validation_index
validation_index = {}
if emu_profiles:
validation_index = _build_validation_index(emu_profiles)
@@ -367,7 +367,6 @@ def generate_pack(
# In md5 mode: validation downgrades OK to UNTESTED
if (file_status.get(dedup_key) == "ok"
and local_path and validation_index):
from verify import check_file_validation
fname = file_entry.get("name", "")
reason = check_file_validation(local_path, fname, validation_index)
if reason:
@@ -523,19 +522,6 @@ def _normalize_zip_for_pack(source_zip: str, dest_path: str, target_zf: zipfile.
# Emulator/system mode pack generation
# ---------------------------------------------------------------------------
def _filter_files_by_mode(files: list[dict], standalone: bool) -> list[dict]:
"""Filter file entries by libretro/standalone mode."""
result = []
for f in files:
fmode = f.get("mode", "")
if standalone and fmode == "libretro":
continue
if not standalone and fmode == "standalone":
continue
result.append(f)
return result
def _resolve_destination(file_entry: dict, pack_structure: dict | None,
standalone: bool) -> str:
"""Resolve the ZIP destination path for a file entry."""
@@ -620,7 +606,7 @@ def generate_emulator_pack(
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
for emu_name, profile in sorted(selected):
pack_structure = profile.get("pack_structure")
files = _filter_files_by_mode(profile.get("files", []), standalone)
files = filter_files_by_mode(profile.get("files", []), standalone)
for dd in profile.get("data_directories", []):
ref_key = dd.get("ref", "")
if not ref_key or not data_registry or ref_key not in data_registry:
@@ -825,34 +811,6 @@ def generate_system_pack(
return result
def _list_emulators_pack(emulators_dir: str) -> None:
"""Print available emulator profiles for pack generation."""
profiles = load_emulator_profiles(emulators_dir, skip_aliases=False)
for name in sorted(profiles):
p = profiles[name]
if p.get("type") in ("alias", "test"):
continue
display = p.get("emulator", name)
ptype = p.get("type", "libretro")
systems = ", ".join(p.get("systems", [])[:3])
more = "..." if len(p.get("systems", [])) > 3 else ""
print(f" {name:30s} {display:40s} [{ptype}] {systems}{more}")
def _list_systems_pack(emulators_dir: str) -> None:
"""Print available system IDs with emulator count."""
profiles = load_emulator_profiles(emulators_dir)
system_emus: dict[str, list[str]] = {}
for name, p in profiles.items():
if p.get("type") in ("alias", "test", "launcher"):
continue
for sys_id in p.get("systems", []):
system_emus.setdefault(sys_id, []).append(name)
for sys_id in sorted(system_emus):
count = len(system_emus[sys_id])
print(f" {sys_id:35s} ({count} emulator{'s' if count > 1 else ''})")
def list_platforms(platforms_dir: str) -> list[str]:
"""List available platform names from YAML files."""
platforms = []
@@ -893,10 +851,10 @@ def main():
print(p)
return
if args.list_emulators:
_list_emulators_pack(args.emulators_dir)
list_emulator_profiles(args.emulators_dir)
return
if args.list_systems:
_list_systems_pack(args.emulators_dir)
list_system_ids(args.emulators_dir)
return
# Mutual exclusion
@@ -1022,10 +980,15 @@ def verify_pack(zip_path: str, db: dict) -> tuple[bool, dict]:
if name.startswith("INSTRUCTIONS_") or name == "manifest.json":
continue
with zf.open(info) as f:
data = f.read()
sha1 = hashlib.sha1(data).hexdigest()
md5 = hashlib.md5(data).hexdigest()
size = len(data)
sha1_h = hashlib.sha1()
md5_h = hashlib.md5()
size = 0
for chunk in iter(lambda: f.read(65536), b""):
sha1_h.update(chunk)
md5_h.update(chunk)
size += len(chunk)
sha1 = sha1_h.hexdigest()
md5 = md5_h.hexdigest()
# Look up in database: files_db keyed by SHA1
db_entry = files_db.get(sha1)
@@ -1080,25 +1043,33 @@ def verify_pack(zip_path: str, db: dict) -> tuple[bool, dict]:
def inject_manifest(zip_path: str, manifest: dict) -> None:
"""Inject manifest.json into an existing ZIP pack."""
import tempfile as _tempfile
manifest_json = json.dumps(manifest, indent=2, ensure_ascii=False)
# ZipFile doesn't support appending to existing entries,
# so we rebuild with the manifest added
tmp_fd, tmp_path = _tempfile.mkstemp(suffix=".zip", dir=os.path.dirname(zip_path))
os.close(tmp_fd)
try:
with zipfile.ZipFile(zip_path, "r") as src, \
zipfile.ZipFile(tmp_path, "w", zipfile.ZIP_DEFLATED) as dst:
for item in src.infolist():
if item.filename == "manifest.json":
continue # replace existing
dst.writestr(item, src.read(item.filename))
dst.writestr("manifest.json", manifest_json)
os.replace(tmp_path, zip_path)
except Exception:
os.unlink(tmp_path)
raise
# Check if manifest already exists
with zipfile.ZipFile(zip_path, "r") as zf:
has_manifest = "manifest.json" in zf.namelist()
if not has_manifest:
# Fast path: append directly
with zipfile.ZipFile(zip_path, "a") as zf:
zf.writestr("manifest.json", manifest_json)
else:
# Rebuild to replace existing manifest
import tempfile as _tempfile
tmp_fd, tmp_path = _tempfile.mkstemp(suffix=".zip", dir=os.path.dirname(zip_path))
os.close(tmp_fd)
try:
with zipfile.ZipFile(zip_path, "r") as src, \
zipfile.ZipFile(tmp_path, "w", zipfile.ZIP_DEFLATED) as dst:
for item in src.infolist():
if item.filename == "manifest.json":
continue
dst.writestr(item, src.read(item.filename))
dst.writestr("manifest.json", manifest_json)
os.replace(tmp_path, zip_path)
except (OSError, zipfile.BadZipFile):
os.unlink(tmp_path)
raise
def generate_sha256sums(output_dir: str) -> str | None:

View File

@@ -198,11 +198,22 @@ def _download_and_extract(
shutil.copyfileobj(src, dst)
file_count += 1
# atomic swap: remove old cache, move new into place
if cache_dir.exists():
shutil.rmtree(cache_dir)
# atomic swap: rename old before moving new into place
cache_dir.parent.mkdir(parents=True, exist_ok=True)
shutil.move(str(extract_dir), str(cache_dir))
old_cache = cache_dir.with_suffix(".old")
if cache_dir.exists():
if old_cache.exists():
shutil.rmtree(old_cache)
cache_dir.rename(old_cache)
try:
shutil.move(str(extract_dir), str(cache_dir))
except OSError:
# Restore old cache on failure
if old_cache.exists() and not cache_dir.exists():
old_cache.rename(cache_dir)
raise
if old_cache.exists():
shutil.rmtree(old_cache)
return file_count

View File

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

View File

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

View File

@@ -93,7 +93,10 @@ class ValidationResult:
def load_database(db_path: str) -> dict | None:
try:
return _load_database(db_path)
except (FileNotFoundError, json.JSONDecodeError):
except FileNotFoundError:
return None
except json.JSONDecodeError as e:
print(f"WARNING: corrupt database.json: {e}", file=sys.stderr)
return None

View File

@@ -35,13 +35,12 @@ except ImportError:
sys.path.insert(0, os.path.dirname(__file__))
from common import (
build_zip_contents_index, check_inside_zip, compute_hashes,
group_identical_platforms, load_data_dir_registry,
load_emulator_profiles, load_platform_config,
_build_validation_index, build_zip_contents_index, check_file_validation,
check_inside_zip, compute_hashes, filter_files_by_mode,
group_identical_platforms, list_emulator_profiles, list_system_ids,
load_data_dir_registry, load_emulator_profiles, load_platform_config,
md5sum, md5_composite, resolve_local_file, resolve_platform_cores,
)
from crypto_verify import check_crypto_validation
DEFAULT_DB = "database.json"
DEFAULT_PLATFORMS_DIR = "platforms"
DEFAULT_EMULATORS_DIR = "emulators"
@@ -68,173 +67,6 @@ _STATUS_ORDER = {Status.OK: 0, Status.UNTESTED: 1, Status.MISSING: 2}
_SEVERITY_ORDER = {Severity.OK: 0, Severity.INFO: 1, Severity.WARNING: 2, Severity.CRITICAL: 3}
# ---------------------------------------------------------------------------
# Emulator-level validation (size, crc32 checks from emulator profiles)
# ---------------------------------------------------------------------------
def _parse_validation(validation: list | dict | None) -> list[str]:
"""Extract the validation check list from a file's validation field.
Handles both simple list and divergent (core/upstream) dict forms.
For dicts, uses the ``core`` key since RetroArch users run the core.
"""
if validation is None:
return []
if isinstance(validation, list):
return validation
if isinstance(validation, dict):
return validation.get("core", [])
return []
# Validation types that require console-specific cryptographic keys.
# verify.py cannot reproduce these — size checks still apply if combined.
_CRYPTO_CHECKS = frozenset({"signature", "crypto"})
# All reproducible validation types.
_HASH_CHECKS = frozenset({"crc32", "md5", "sha1", "adler32"})
def _build_validation_index(profiles: dict) -> dict[str, dict]:
"""Build per-filename validation rules from emulator profiles.
Returns {filename: {"checks": [str], "size": int|None, "min_size": int|None,
"max_size": int|None, "crc32": str|None, "md5": str|None, "sha1": str|None,
"adler32": str|None, "crypto_only": [str]}}.
``crypto_only`` lists validation types we cannot reproduce (signature, crypto)
so callers can report them as non-verifiable rather than silently skipping.
When multiple emulators reference the same file, merges checks (union).
Raises ValueError if two profiles declare conflicting values.
"""
index: dict[str, dict] = {}
sources: dict[str, dict[str, str]] = {}
for emu_name, profile in profiles.items():
if profile.get("type") in ("launcher", "alias"):
continue
for f in profile.get("files", []):
fname = f.get("name", "")
if not fname:
continue
checks = _parse_validation(f.get("validation"))
if not checks:
continue
if fname not in index:
index[fname] = {
"checks": set(), "sizes": set(),
"min_size": None, "max_size": None,
"crc32": set(), "md5": set(), "sha1": set(), "sha256": set(),
"adler32": set(), "crypto_only": set(),
}
sources[fname] = {}
index[fname]["checks"].update(checks)
# Track non-reproducible crypto checks
index[fname]["crypto_only"].update(
c for c in checks if c in _CRYPTO_CHECKS
)
# Size checks
if "size" in checks:
if f.get("size") is not None:
index[fname]["sizes"].add(f["size"])
if f.get("min_size") is not None:
cur = index[fname]["min_size"]
index[fname]["min_size"] = min(cur, f["min_size"]) if cur is not None else f["min_size"]
if f.get("max_size") is not None:
cur = index[fname]["max_size"]
index[fname]["max_size"] = max(cur, f["max_size"]) if cur is not None else f["max_size"]
# Hash checks — collect all accepted hashes as sets (multiple valid
# versions of the same file, e.g. MT-32 ROM versions)
if "crc32" in checks and f.get("crc32"):
norm = f["crc32"].lower()
if norm.startswith("0x"):
norm = norm[2:]
index[fname]["crc32"].add(norm)
for hash_type in ("md5", "sha1", "sha256"):
if hash_type in checks and f.get(hash_type):
index[fname][hash_type].add(f[hash_type].lower())
# Adler32 — stored as known_hash_adler32 field (not in validation: list
# for Dolphin, but support it in both forms for future profiles)
adler_val = f.get("known_hash_adler32") or f.get("adler32")
if adler_val:
norm = adler_val.lower()
if norm.startswith("0x"):
norm = norm[2:]
index[fname]["adler32"].add(norm)
# Convert sets to sorted tuples/lists for determinism
for v in index.values():
v["checks"] = sorted(v["checks"])
v["crypto_only"] = sorted(v["crypto_only"])
# Keep hash sets as frozensets for O(1) lookup in check_file_validation
return index
def check_file_validation(
local_path: str, filename: str, validation_index: dict[str, dict],
bios_dir: str = "bios",
) -> str | None:
"""Check emulator-level validation on a resolved file.
Supports: size (exact/min/max), crc32, md5, sha1, adler32,
signature (RSA-2048 PKCS1v15 SHA256), crypto (AES-128-CBC + SHA256).
Returns None if all checks pass or no validation applies.
Returns a reason string if a check fails.
"""
entry = validation_index.get(filename)
if not entry:
return None
checks = entry["checks"]
# Size checks — sizes is a set of accepted values
if "size" in checks:
actual_size = os.path.getsize(local_path)
if entry["sizes"] and actual_size not in entry["sizes"]:
expected = ",".join(str(s) for s in sorted(entry["sizes"]))
return f"size mismatch: got {actual_size}, accepted [{expected}]"
if entry["min_size"] is not None and actual_size < entry["min_size"]:
return f"size too small: min {entry['min_size']}, got {actual_size}"
if entry["max_size"] is not None and actual_size > entry["max_size"]:
return f"size too large: max {entry['max_size']}, got {actual_size}"
# Hash checks — compute once, reuse for all hash types.
# Each hash field is a set of accepted values (multiple valid ROM versions).
need_hashes = (
any(h in checks and entry.get(h) for h in ("crc32", "md5", "sha1", "sha256"))
or entry.get("adler32")
)
if need_hashes:
hashes = compute_hashes(local_path)
if "crc32" in checks and entry["crc32"]:
if hashes["crc32"].lower() not in entry["crc32"]:
expected = ",".join(sorted(entry["crc32"]))
return f"crc32 mismatch: got {hashes['crc32']}, accepted [{expected}]"
if "md5" in checks and entry["md5"]:
if hashes["md5"].lower() not in entry["md5"]:
expected = ",".join(sorted(entry["md5"]))
return f"md5 mismatch: got {hashes['md5']}, accepted [{expected}]"
if "sha1" in checks and entry["sha1"]:
if hashes["sha1"].lower() not in entry["sha1"]:
expected = ",".join(sorted(entry["sha1"]))
return f"sha1 mismatch: got {hashes['sha1']}, accepted [{expected}]"
if "sha256" in checks and entry["sha256"]:
if hashes["sha256"].lower() not in entry["sha256"]:
expected = ",".join(sorted(entry["sha256"]))
return f"sha256 mismatch: got {hashes['sha256']}, accepted [{expected}]"
if entry["adler32"]:
if hashes["adler32"].lower() not in entry["adler32"]:
expected = ",".join(sorted(entry["adler32"]))
return f"adler32 mismatch: got 0x{hashes['adler32']}, accepted [{expected}]"
# Signature/crypto checks (3DS RSA, AES)
if entry["crypto_only"]:
crypto_reason = check_crypto_validation(local_path, filename, bios_dir)
if crypto_reason:
return crypto_reason
return None
# ---------------------------------------------------------------------------
# Verification functions
# ---------------------------------------------------------------------------
@@ -269,7 +101,7 @@ def verify_entry_md5(
base = {"name": name, "required": required}
if expected_md5 and "," in expected_md5:
md5_list = [m.strip() for m in expected_md5.split(",") if m.strip()]
md5_list = [m.strip().lower() for m in expected_md5.split(",") if m.strip()]
else:
md5_list = [expected_md5] if expected_md5 else []
@@ -695,19 +527,6 @@ def print_platform_result(result: dict, group: list[str]) -> None:
# Emulator/system mode verification
# ---------------------------------------------------------------------------
def _filter_files_by_mode(files: list[dict], standalone: bool) -> list[dict]:
"""Filter file entries by libretro/standalone mode."""
result = []
for f in files:
fmode = f.get("mode", "")
if standalone and fmode == "libretro":
continue
if not standalone and fmode == "standalone":
continue
result.append(f)
return result
def _effective_validation_label(details: list[dict], validation_index: dict) -> str:
"""Determine the bracket label for the report.
@@ -783,7 +602,7 @@ def verify_emulator(
data_dir_notices: list[str] = []
for emu_name, profile in selected:
files = _filter_files_by_mode(profile.get("files", []), standalone)
files = filter_files_by_mode(profile.get("files", []), standalone)
# Check data directories (only notice if not cached)
for dd in profile.get("data_directories", []):
@@ -976,34 +795,6 @@ def print_emulator_result(result: dict) -> None:
print(f" Note: data directory '{ref}' required but not included (use refresh_data_dirs.py)")
def _list_emulators(emulators_dir: str) -> None:
"""Print available emulator profiles."""
profiles = load_emulator_profiles(emulators_dir)
for name in sorted(profiles):
p = profiles[name]
if p.get("type") in ("alias", "test"):
continue
display = p.get("emulator", name)
ptype = p.get("type", "libretro")
systems = ", ".join(p.get("systems", [])[:3])
more = "..." if len(p.get("systems", [])) > 3 else ""
print(f" {name:30s} {display:40s} [{ptype}] {systems}{more}")
def _list_systems(emulators_dir: str) -> None:
"""Print available system IDs with emulator count."""
profiles = load_emulator_profiles(emulators_dir)
system_emus: dict[str, list[str]] = {}
for name, p in profiles.items():
if p.get("type") in ("alias", "test", "launcher"):
continue
for sys_id in p.get("systems", []):
system_emus.setdefault(sys_id, []).append(name)
for sys_id in sorted(system_emus):
count = len(system_emus[sys_id])
print(f" {sys_id:35s} ({count} emulator{'s' if count > 1 else ''})")
def main():
parser = argparse.ArgumentParser(description="Platform-native BIOS verification")
parser.add_argument("--platform", "-p", help="Platform name")
@@ -1021,10 +812,10 @@ def main():
args = parser.parse_args()
if args.list_emulators:
_list_emulators(args.emulators_dir)
list_emulator_profiles(args.emulators_dir)
return
if args.list_systems:
_list_systems(args.emulators_dir)
list_system_ids(args.emulators_dir)
return
# Mutual exclusion

View File

@@ -29,14 +29,15 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "scripts"))
import yaml
from common import (
build_zip_contents_index, check_inside_zip, group_identical_platforms,
load_emulator_profiles, load_platform_config, md5_composite, md5sum,
resolve_local_file, resolve_platform_cores,
_build_validation_index, build_zip_contents_index, check_file_validation,
check_inside_zip, compute_hashes, filter_files_by_mode,
group_identical_platforms, load_emulator_profiles, load_platform_config,
md5_composite, md5sum, parse_md5_list, resolve_local_file,
resolve_platform_cores, safe_extract_zip,
)
from verify import (
Severity, Status, verify_platform, find_undeclared_files, find_exclusion_notes,
_build_validation_index, check_file_validation, verify_emulator,
_filter_files_by_mode, _effective_validation_label,
verify_emulator, _effective_validation_label,
)
@@ -58,6 +59,10 @@ class TestE2E(unittest.TestCase):
# ---------------------------------------------------------------
def setUp(self):
# Clear emulator profile cache to avoid stale data between tests
from common import _emulator_profiles_cache
_emulator_profiles_cache.clear()
self.root = tempfile.mkdtemp()
self.bios_dir = os.path.join(self.root, "bios")
self.platforms_dir = os.path.join(self.root, "platforms")
@@ -967,16 +972,16 @@ class TestE2E(unittest.TestCase):
# test_validation has crc32, md5, sha1, size → all listed
self.assertEqual(result["verification_mode"], "crc32+md5+sha1+signature+size")
def test_99_filter_files_by_mode(self):
"""_filter_files_by_mode correctly filters standalone/libretro."""
def test_99filter_files_by_mode(self):
"""filter_files_by_mode correctly filters standalone/libretro."""
files = [
{"name": "a.bin"}, # no mode → both
{"name": "b.bin", "mode": "libretro"}, # libretro only
{"name": "c.bin", "mode": "standalone"}, # standalone only
{"name": "d.bin", "mode": "both"}, # explicit both
]
lr = _filter_files_by_mode(files, standalone=False)
sa = _filter_files_by_mode(files, standalone=True)
lr = filter_files_by_mode(files, standalone=False)
sa = filter_files_by_mode(files, standalone=True)
lr_names = {f["name"] for f in lr}
sa_names = {f["name"] for f in sa}
self.assertEqual(lr_names, {"a.bin", "b.bin", "d.bin"})
@@ -1012,5 +1017,86 @@ class TestE2E(unittest.TestCase):
self.assertGreater(result["severity_counts"][Severity.WARNING], 0)
def test_102_safe_extract_zip_blocks_traversal(self):
"""safe_extract_zip must reject zip-slip path traversal."""
malicious_zip = os.path.join(self.root, "evil.zip")
with zipfile.ZipFile(malicious_zip, "w") as zf:
zf.writestr("../../etc/passwd", "root:x:0:0")
dest = os.path.join(self.root, "extract_dest")
os.makedirs(dest)
with self.assertRaises(ValueError):
safe_extract_zip(malicious_zip, dest)
def test_103_safe_extract_zip_normal(self):
"""safe_extract_zip extracts valid files correctly."""
normal_zip = os.path.join(self.root, "normal.zip")
with zipfile.ZipFile(normal_zip, "w") as zf:
zf.writestr("subdir/file.txt", "hello")
dest = os.path.join(self.root, "extract_normal")
os.makedirs(dest)
safe_extract_zip(normal_zip, dest)
extracted = os.path.join(dest, "subdir", "file.txt")
self.assertTrue(os.path.exists(extracted))
with open(extracted) as f:
self.assertEqual(f.read(), "hello")
def test_104_compute_hashes_correctness(self):
"""compute_hashes returns correct values for known content."""
test_file = os.path.join(self.root, "hash_test.bin")
data = b"retrobios test content"
with open(test_file, "wb") as f:
f.write(data)
import zlib
expected_sha1 = hashlib.sha1(data).hexdigest()
expected_md5 = hashlib.md5(data).hexdigest()
expected_sha256 = hashlib.sha256(data).hexdigest()
expected_crc32 = format(zlib.crc32(data) & 0xFFFFFFFF, "08x")
result = compute_hashes(test_file)
self.assertEqual(result["sha1"], expected_sha1)
self.assertEqual(result["md5"], expected_md5)
self.assertEqual(result["sha256"], expected_sha256)
self.assertEqual(result["crc32"], expected_crc32)
def test_105_resolve_with_empty_database(self):
"""resolve_local_file handles empty database gracefully."""
empty_db = {"files": {}, "indexes": {"by_md5": {}, "by_name": {}, "by_path_suffix": {}}}
entry = {"name": "nonexistent.bin", "sha1": "abc123"}
path, status = resolve_local_file(entry, empty_db)
self.assertIsNone(path)
self.assertEqual(status, "not_found")
def test_106_parse_md5_list(self):
"""parse_md5_list normalizes comma-separated MD5s."""
self.assertEqual(parse_md5_list(""), [])
self.assertEqual(parse_md5_list("ABC123"), ["abc123"])
self.assertEqual(parse_md5_list("abc, DEF , ghi"), ["abc", "def", "ghi"])
self.assertEqual(parse_md5_list(",,,"), [])
def test_107filter_files_by_mode(self):
"""filter_files_by_mode filters standalone/libretro correctly."""
files = [
{"name": "a.bin", "mode": "standalone"},
{"name": "b.bin", "mode": "libretro"},
{"name": "c.bin", "mode": "both"},
{"name": "d.bin"}, # no mode
]
# Libretro mode: exclude standalone
result = filter_files_by_mode(files, standalone=False)
names = [f["name"] for f in result]
self.assertNotIn("a.bin", names)
self.assertIn("b.bin", names)
self.assertIn("c.bin", names)
self.assertIn("d.bin", names)
# Standalone mode: exclude libretro
result = filter_files_by_mode(files, standalone=True)
names = [f["name"] for f in result]
self.assertIn("a.bin", names)
self.assertNotIn("b.bin", names)
self.assertIn("c.bin", names)
self.assertIn("d.bin", names)
if __name__ == "__main__":
unittest.main()