chore: merge site and docs fixes, fifth pass

This commit is contained in:
Abdessamad Derraz committed 2026-11-06 17:15:57 +01:00
commit b9f309940c
67 files changed
+1495 -786

No files matched your search

+4
View File
@@ -38,6 +38,7 @@ on:
- "scripts/packreadme.py"
- "scripts/packresolve.py"
- "scripts/packverify.py"
- "scripts/pr_checks.py"
- "scripts/provenance_report.py"
- "scripts/refresh_data_dirs.py"
- "scripts/region.py"
@@ -46,6 +47,7 @@ on:
- "scripts/safeparse.py"
- "scripts/sect233r1.py"
- "scripts/sitebootstrap.py"
- "scripts/sitecollection.py"
- "scripts/sitediagrams.py"
- "scripts/siteentries.py"
- "scripts/siteguides.py"
@@ -89,6 +91,8 @@ on:
# generated folders are gitignored and never trigger anything.
- "website/**"
- ".github/workflows/deploy-site.yml"
# The contributing guide quotes the pip lines of its test job.
- ".github/workflows/validate.yml"
workflow_dispatch:
permissions:
+25 -10
View File
@@ -72,18 +72,33 @@ validation above is its check. For code, profiles or platform files, these are
the checks CI runs on a pull request (Python 3.12 or newer):
```bash
pip install pyyaml "jsonschema[format-nongpl]" markdown pyte hypothesis vermin setuptools
python scripts/validate_pr.py --changed # the BIOS files the branch adds
python scripts/validate_schemas.py --source-only # profiles and platform files
python scripts/profile_sync.py --added --strict # each profile the branch adds
python scripts/refresh_data_dirs.py # data directories the pack-building tests read
python scripts/generate_site.py # the pages the site tests read
npm ci --prefix website && npm run build --prefix website # the built site, Node 24
python -m unittest discover -s tests -t .
python scripts/validate_site.py --site-dir website/dist # links and page budgets
vermin --no-tips -t=3.8- --violations install.py # install.py stays on Python 3.8
pip install pyyaml "jsonschema[format-nongpl]==4.26.0" "markdown==3.10.2" "pyte==0.8.2" "hypothesis==6.170.8" "vermin==1.9.2" "setuptools==80.9.0"
python scripts/pr_checks.py
```
`pr_checks.py` runs these, in CI's order, carries on past a failure as CI
does, and names at the end each check that failed or could not run:
```bash
python scripts/validate_pr.py --changed # the BIOS files the branch adds
python scripts/validate_schemas.py --source-only # profiles and platform files
python scripts/profile_sync.py --added --strict # each profile the branch adds
vermin --no-tips -t=3.8- --violations install.py # install.py stays on Python 3.8
python scripts/export_native.py --all --fetch --output-dir tmp/native # files the exporter tests compare with
python scripts/refresh_data_dirs.py # data directories the pack-building tests read
python scripts/generate_site.py # the pages the site tests read
npm ci --prefix website # the site's packages, Node 24
npm run build --prefix website # the built site
python -m unittest discover -s tests -t . # the suite
python3.8 -m unittest tests.test_install_tui # the full screen on the installer's floor
python scripts/validate_site.py --site-dir website/dist # links and page budgets
```
The versions are the ones CI pins. Three tools come from the system, not from
that line, and the tests that need them skip without them: `shellcheck` for the
handheld launchers, `bwrap` (bubblewrap) for the probe sandbox, and a Python 3.8
with pyte (`python3.8 -m pip install "pyte==0.8.2"`) for the full screen. CI has all three.
`profile_sync.py` compares the branch with this repository's `main`, and
fetches it in a fork's clone. A profile you only edit is not held to
`--strict`: its older refs are reviewed by hand.
+1 -1
View File
@@ -124,7 +124,7 @@ Checked by is the test your platform runs on its own, replicated here from its s
## Build your own pack
Clone the repo and generate packs for any platform, emulator, or system:
Packs are built from a clone, and the clone carries the collection: its checkout alone is over 7.7 GB. `git clone --depth 1 https://github.com/Abdess/retrobios.git` leaves the history out. Files kept as release assets are downloaded when a pack needs them. Then generate packs for any platform, emulator, or system:
```bash
# The pack of a platform, as released
+3 -3
View File
@@ -67,6 +67,6 @@ platform_details:
size: 1536
note: |
Empty flashcart header written to the first 1536 bytes of the
W25Q128 flash image during initialization. This is not a BIOS
file -- it is the default flash content the Arduboy FX expects
at the start of external storage.
W25Q128 flash image during initialization. It is the default flash
content the Arduboy FX expects at the start of external storage,
not a BIOS file.
+4 -5
View File
@@ -16,7 +16,7 @@ cores: [atari800]
systems: [atari-400-800, atari-5200, xegs]
# Atari800 emulates the Atari 8-bit family (400/800/XL/XE) and the 5200 console.
# All BIOS files are optional -- the core ships built-in Altirra OS replacements
# All BIOS files are optional. The core ships built-in Altirra OS replacements
# (altirraos_800, altirraos_xl, altirra_5200_os, altirra_basic) compiled into the
# binary. If real ROM files are found in the system directory they take priority.
#
@@ -121,9 +121,8 @@ files:
note: >
Atari 400/800 OS Rev B (NTSC). CRC32 0x0e86d61d matches sysrom.c
SYSROM_B_NTSC. Preferred over Rev A for 400/800 mode.
Note: .info lists md5 a3e8d617c95d08031fe1b20d541434b2 which is a
different dump (crc32 3e28a1fe) -- both in repo, the CRC32-matched
version is the one sysrom.c auto-detects.
The .info lists md5 a3e8d617c95d08031fe1b20d541434b2, a different
dump (crc32 3e28a1fe). sysrom.c detects the CRC32-matched one.
source_ref: "source atari800/src/sysrom.c:91, source atari800/src/sysrom.c:286"
# -- Atari XL/XE OS --
@@ -165,7 +164,7 @@ files:
# -- XEGS Missile Command built-in game --
# The XEGS had Missile Command in ROM (8 KB), CRC32: 0xbdca01fb
# No filename auto-match -- must be set via config or detected by CRC.
# No filename auto-match. It must be set via config or detected by CRC.
- name: "XEGAME.ROM"
path: "XEGAME.ROM"
+1 -1
View File
@@ -17,7 +17,7 @@ systems:
notes: |
Unfinished Picross puzzle game, part of the libretro-samples repository.
No separate upstream -- written directly for the libretro API.
The libretro API is its only target, with no separate upstream.
supports_no_game true (tests/cruzes/cruzes.c:772), valid_extensions "" (tests/cruzes/cruzes.c:749).
Puzzle grid hardcoded in retro_init (tests/cruzes/cruzes.c:706-717) via load_challenge.
-1
View File
@@ -51,4 +51,3 @@ platform_details:
note: |
No external BIOS. The 80-byte character font is compiled into the
binary as char_mem[] and memcpy'd into RAM[0x000] on every reset.
This is standard CHIP-8 behavior -- all interpreters embed the font.
+2 -2
View File
@@ -30,8 +30,8 @@ notes: |
binary as string constants (p8GlobalLuaFunctions.h, DefaultCart.h).
When no ROM is passed (supports_no_game = true), the core queues its
built-in BIOS cart "__FAKE08-BIOS.p8" (platform/libretro/libretro.cpp:751). This is not
an external file -- it is generated internally by the VM.
built-in BIOS cart "__FAKE08-BIOS.p8" (platform/libretro/libretro.cpp:751). The VM
generates it internally, so no external file is read.
The core accepts .p8 (text) and .png (cartridge-as-image) formats
(platform/libretro/libretro.cpp:258). Content is loaded either via the libretro data buffer
+1 -1
View File
@@ -35,7 +35,7 @@ notes: |
hle_state.psu1_hle and hle_state.psu2_hle (src/channelf_hle.c).
The .info file declares all three firmware files as required (opt=false),
but the core does not abort on missing BIOS -- it falls back to HLE.
but the core does not abort on a missing BIOS. It falls back to HLE.
files:
- name: "sl31253.bin"
+2 -2
View File
@@ -20,7 +20,7 @@ systems:
# Z80-based at 3.072 MHz, PAL 50fps, character-mode display (32x16 chars, 256x208 px).
#
# The core embeds all three firmware files compressed in rom.c (LoadCHRGENBIN,
# LoadROM1BIN, LoadROM2BIN). External files are optional overrides -- if ANY of
# LoadROM1BIN, LoadROM2BIN). External files are optional overrides. If ANY of
# CHRGEN.BIN or ROM1.BIN is missing from <system_dir>/galaksija/, the core sets
# firmware_ignore=true and loads all ROMs from the embedded copies instead.
#
@@ -73,7 +73,7 @@ files:
note: >
Math extension ROM. Mapped at 0x1000-0x1FFF. Adds floating-point math
functions (SIN, COS, TG, SQR, LN, EXP, POW, etc). Optional even when
loading from disk -- missing ROM2 only triggers a warning. Embedded in
loading from disk, where a missing ROM2 only triggers a warning. Embedded in
rom.c as fallback.
source_ref: "source src/rom.h:11, source src/libretro.c:107-110, source src/external/galaksija-emulator/src/init.c:83-92"
+1 -1
View File
@@ -42,7 +42,7 @@ notes: |
File path construction: retro_load_game() sets
FolderUtilities::SetHomeFolder(systemFolder). All BIOS lookups use
CombinePath(GetHomeFolder(), filename) -- flat, no subdirectories.
CombinePath(GetHomeFolder(), filename), flat, with no subdirectories.
No functional divergence between upstream and libretro for BIOS loading.
+1 -1
View File
@@ -30,7 +30,7 @@ notes: |
All BIOS files are optional. mGBA ships a built-in HLE BIOS for GBA
(src/gba/hle-bios.c) that covers most software. The real BIOS improves
accuracy (startup logo, some edge-case SWI behavior). There is no HLE
BIOS for GB/GBC/SGB -- those rely on GBSkipBIOS() if no file is found.
BIOS for GB/GBC/SGB, which rely on GBSkipBIOS() if no file is found.
BIOS loading is controlled by two core options:
mgba_use_bios (default ON) - whether to look for BIOS files at all
+2 -2
View File
@@ -21,8 +21,8 @@ notes: |
The core has an embedded FreeBIOS (open-source replacement) compiled
directly into the binary from freebios/freebios.c. This means the
real BIOS is never strictly required -- games will run with FreeBIOS
alone, though compatibility may differ from the original firmware.
real BIOS is never strictly required. Games run with FreeBIOS alone,
though compatibility may differ from the original firmware.
BIOS loading sequence in the libretro port:
+14 -14
View File
@@ -120,7 +120,7 @@ files:
analysis:
engine_wad_contents:
source_ref: "data/CMakeLists.txt:24-340 (rdatawad input lists), data/CMakeLists.txt:342-362 (WAD_SRC and WAD_CMDLINE)"
source_ref: "prboom2/data/CMakeLists.txt:24-340 (rdatawad input lists), prboom2/data/CMakeLists.txt:342-362 (WAD_SRC and WAD_CMDLINE)"
note: >
What rdatawad packs into prboom-plus.wad, in the order the command line
builds it.
@@ -135,8 +135,8 @@ analysis:
- "SINETABL, TANGTABL, TANTOANG, GAMMATBL"
- "CHEXDEH, BFGBEX, NERVEBEX"
- "GLSHADOW, GLFP, GLVP"
- "-PRBHUD-, read by hu_stuff.c:1144"
- "the M_* OpenGL automap icons listed in gl_map.c:48-68"
- "-PRBHUD-, read by prboom2/src/hu_stuff.c:1144"
- "the M_* OpenGL automap icons listed in prboom2/src/gl_map.c:48-68"
sounds: "DSDGSIT, DSDGATK, DSDGACT, DSDGDTH, DSDGPAIN"
graphics: "the DIG* status bar digits, STBR*, the BOX* frame pieces, STKEYS6-8, STCFN096, M_BUTT1/2, M_COLORS, M_PALNO, M_PALSEL, M_VBOX, CROSS1-3"
flats: "-N0_TEX-, between FF_START and FF_END"
@@ -146,7 +146,7 @@ analysis:
files:
- "shaders/glvp.txt"
- "shaders/glfp.txt"
source_ref: "gl_shader.c:144-152 (path built from I_DoomExeDir), gl_shader.c:84-129 (ReadLump)"
source_ref: "prboom2/src/gl_shader.c:144-152 (path built from I_DoomExeDir), prboom2/src/gl_shader.c:84-129 (ReadLump)"
note: >
Read from the config directory when present. ReadLump falls back to the
GLVP and GLFP lumps in the ns_prboom namespace when the file is missing,
@@ -160,18 +160,18 @@ analysis:
- filename: "prboom-plus.cfg"
source_ref: "prboom2/src/m_misc.c:1586-1600 (BOOM_CFG, path from I_DoomExeDir), prboom2/src/m_misc.c:1606 (read)"
- filename: "RGB2PAL.dat"
source_ref: "gl_hires.c:938-953 (read), gl_hires.c:962-967 (opened wb and written)"
source_ref: "prboom2/src/gl_hires.c:938-953 (read), prboom2/src/gl_hires.c:962-967 (opened wb and written)"
note: >
A 24-bit RGB to palette lookup table built from V_GetPlaypal, so its
contents follow whichever IWAD is loaded. gl_hires.c:920-936 prefers
contents follow whichever IWAD is loaded. prboom2/src/gl_hires.c:920-936 prefers
an RGB2PAL lump of the right length when a WAD supplies one.
- filename: "<hires image>.cache"
source_ref: "gl_hires.c:1144-1147 (read), gl_hires.c:1179 (gld_HiRes_WriteCache)"
source_ref: "prboom2/src/gl_hires.c:1144-1147 (read), prboom2/src/gl_hires.c:1179 (gld_HiRes_WriteCache)"
note: "Keyed on the source image mtime and discarded when it differs."
- filename: "levelstat.txt"
source_ref: "e6y.c:968"
source_ref: "prboom2/src/e6y.c:968"
- filename: "levelinfo.txt"
source_ref: "prboom2/src/d_main.c:1049-1060, behind GL_DOOM and PRBOOM_DEBUG"
source_ref: "prboom2/src/d_main.c:1049-1060 (behind GL_DOOM and PRBOOM_DEBUG)"
- filename: "temp_a.nut, temp_v.nut"
source_ref: "prboom2/src/m_misc.c:1065-1066 (cap_tempfile1, cap_tempfile2)"
@@ -189,11 +189,11 @@ analysis:
- what: "autoload directories, <config dir>/autoload/doom-all and one per IWAD"
source_ref: "prboom2/src/d_main.c:1400-1414 (GetAutoloadBaseDir), prboom2/src/d_main.c:1500-1519 (D_AutoloadIWadDir)"
- what: "demos, -playdemo, -fastdemo, -record"
source_ref: "r_demo.c:89-119, g_game.c:2570, g_game.c:3150"
source_ref: "prboom2/src/r_demo.c:89-119, prboom2/src/g_game.c:2570, prboom2/src/g_game.c:3150"
- what: "external high resolution texture packs, under textures/, flats/ and patches/"
source_ref: "gl_hires.c:467-631 (hires_paths), gl_hires.c:703-719 (gl_texture_hires_dir or I_DoomExeDir)"
source_ref: "prboom2/src/gl_hires.c:467-631 (hires_paths), prboom2/src/gl_hires.c:703-719 (gl_texture_hires_dir or I_DoomExeDir)"
- what: "savegames, DOOMSAVEDIR or the config directory"
source_ref: "prboom2/src/d_main.c:1019-1032, m_menu.c:957"
source_ref: "prboom2/src/d_main.c:1019-1032, prboom2/src/m_menu.c:957"
- what: "response files, @name"
source_ref: "prboom2/src/d_main.c:1094-1112"
@@ -203,6 +203,6 @@ analysis:
- what: "ffmpeg, video capture and muxing"
source_ref: "prboom2/src/m_misc.c:1062-1064 (cap_soundcommand, cap_videocommand, cap_muxcommand)"
- what: "wget then unzip, fetching a WAD a game server asks for"
source_ref: "d_client.c:236-257 (execlp), doc/prboom-plus.6:200-206"
source_ref: "prboom2/src/d_client.c:236-257 (execlp), prboom2/doc/prboom-plus.6:200-206"
- what: "getwad_cmdline, run through system() when a needed WAD is missing"
source_ref: "r_demo.c:1641-1704 (D_TryGetWad), prboom2/src/m_misc.c:1029 (default empty, so the path is inert until configured)"
source_ref: "prboom2/src/r_demo.c:1641-1704 (D_TryGetWad), prboom2/src/m_misc.c:1029 (default empty, so the path is inert until configured)"
+4 -4
View File
@@ -23,7 +23,7 @@ systems:
# 1. Base path set to <system_dir>/keropi/ via file_setcd(winx68k_dir) at line 969
# 2. IPL ROM: iterates BIOSFILE[] = {iplrom.dat, iplrom30.dat, iplromco.dat, iplromxv.dat}
# and opens the first file found. Reads 0x20000 bytes (128 KB) into IPL buffer.
# Returns 0 (failure) if none found -- core will not start.
# Returns 0 (failure) if none found, and the core does not start.
# 3. SCSI check (WinX68k_SCSICheck): if IPL contains SCSI vectors at offset 0x30600-0x30c00,
# patches a fake SCSI BIOS stub at $fc0000 with "Human68k" signature.
# 4. Byte-swaps IPL in 16-bit pairs (big-endian ROM on little-endian host).
@@ -32,7 +32,7 @@ systems:
#
# SRAM (x68k/sram.c:50-73):
# sram.dat is 16 KB of battery-backed SRAM, loaded on init and saved on cleanup.
# Not a BIOS -- runtime state. Created automatically if missing.
# Not a BIOS but runtime state. Created automatically if missing.
#
# Directory layout expected:
# <system_dir>/keropi/iplrom.dat (or iplrom30.dat, iplromco.dat, iplromxv.dat)
@@ -41,7 +41,7 @@ systems:
# <system_dir>/keropi/config (auto-created, core settings)
files:
# IPL ROM -- the main system BIOS. The core tries four filenames in order and
# IPL ROM, the main system BIOS. The core tries four filenames in order and
# uses the first one found. Only one is needed. iplrom.dat is the original X68000,
# iplrom30.dat is X68030, iplromco.dat is X68000 Compact, iplromxv.dat is X68000 XVI.
@@ -87,7 +87,7 @@ files:
Only used if the three other IPL variants are not found.
source_ref: "source libretro.c:985"
# Font ROM -- character generator ROM for text rendering.
# Font ROM, the character generator ROM for text rendering.
- name: "cgrom.dat"
installed_on: [recalbox]
+1 -1
View File
@@ -18,7 +18,7 @@ systems:
notes: |
Fork of the last GPLv3 release of redream (inolen) before it went closed-source.
The core has HLE bootstrap -- when boot.bin is missing, bios_invalid_instr()
The core has an HLE bootstrap. When boot.bin is missing, bios_invalid_instr()
traps at PC=0x0 and calls bios_boot(), which loads IP.BIN and 1ST_READ.BIN
directly from disc, sets up syscall vectors, and starts execution at 0xac008300.
+3 -3
View File
@@ -864,7 +864,7 @@
}
}
],
"hashes_corrected": [],
"hashes_updated": [],
"hashes_filled": [
"msx/MSXDOS2.ROM md5",
"msx/FMPAC.rom md5",
@@ -877,10 +877,10 @@
"neogeo/aes.zip md5",
"neogeocd/neocdz.zip md5"
],
"sizes_corrected": [],
"sizes_updated": [],
"sizes_removed": [],
"hashes_cleared": [],
"requirements_corrected": 0,
"required_aligned": 0,
"withheld": [],
"found_by_content": 107,
"removal_candidates": [
+6 -6
View File
@@ -213,7 +213,7 @@
}
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "nds",
"name": "dsi_bios7.bin",
"destination": "dsi_bios7.bin",
@@ -222,7 +222,7 @@
"new": "322ceeafb77b0cc96a1fcc6271e0e0ae"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "nds",
"name": "dsi_bios9.bin",
"destination": "dsi_bios9.bin",
@@ -231,7 +231,7 @@
"new": "23031f25602a52c2df32c39554b0c55e"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "msx",
"name": "MSX.ROM",
"destination": "MSX.ROM",
@@ -1560,16 +1560,16 @@
}
}
],
"hashes_corrected": [
"hashes_updated": [
"nds/dsi_bios7.bin md5",
"nds/dsi_bios9.bin md5",
"msx/MSX.ROM md5"
],
"hashes_filled": [],
"sizes_corrected": [],
"sizes_updated": [],
"sizes_removed": [],
"hashes_cleared": [],
"requirements_corrected": 0,
"required_aligned": 0,
"withheld": [],
"found_by_content": 114,
"removal_candidates": [
+3 -3
View File
@@ -14,12 +14,12 @@
"not_kept": 0
},
"changes": [],
"hashes_corrected": [],
"hashes_updated": [],
"hashes_filled": [],
"sizes_corrected": [],
"sizes_updated": [],
"sizes_removed": [],
"hashes_cleared": [],
"requirements_corrected": 0,
"required_aligned": 0,
"withheld": [],
"found_by_content": 0,
"removal_candidates": [],
+3 -3
View File
@@ -14,12 +14,12 @@
"not_kept": 0
},
"changes": [],
"hashes_corrected": [],
"hashes_updated": [],
"hashes_filled": [],
"sizes_corrected": [],
"sizes_updated": [],
"sizes_removed": [],
"hashes_cleared": [],
"requirements_corrected": 0,
"required_aligned": 0,
"withheld": [],
"found_by_content": 107,
"removal_candidates": [
+3 -3
View File
@@ -955,7 +955,7 @@
}
}
],
"hashes_corrected": [],
"hashes_updated": [],
"hashes_filled": [
"msx/MSXDOS2.ROM md5",
"msx/FMPAC.rom md5",
@@ -1005,10 +1005,10 @@
"Cannonball/opr-10268.99 md5",
"RickDangerous/data.zip md5"
],
"sizes_corrected": [],
"sizes_updated": [],
"sizes_removed": [],
"hashes_cleared": [],
"requirements_corrected": 0,
"required_aligned": 0,
"withheld": [],
"found_by_content": 312,
"unattributed": [],
+7 -7
View File
@@ -48,7 +48,7 @@
}
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "neogeo",
"name": "neogeo.zip",
"destination": "neogeo.zip",
@@ -233,7 +233,7 @@
}
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "nds",
"name": "dsi_bios7.bin",
"destination": "dsi_bios7.bin",
@@ -242,7 +242,7 @@
"new": "322ceeafb77b0cc96a1fcc6271e0e0ae"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "nds",
"name": "dsi_bios9.bin",
"destination": "dsi_bios9.bin",
@@ -284,7 +284,7 @@
}
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "msx",
"name": "MSX.ROM",
"destination": "MSX.ROM",
@@ -1393,17 +1393,17 @@
}
}
],
"hashes_corrected": [
"hashes_updated": [
"neogeo/neogeo.zip md5",
"nds/dsi_bios7.bin md5",
"nds/dsi_bios9.bin md5",
"msx/MSX.ROM md5"
],
"hashes_filled": [],
"sizes_corrected": [],
"sizes_updated": [],
"sizes_removed": [],
"hashes_cleared": [],
"requirements_corrected": 0,
"required_aligned": 0,
"withheld": [],
"found_by_content": 114,
"removal_candidates": [
+106 -106
View File
@@ -29,7 +29,7 @@
}
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "Arcade",
"name": "aes.zip",
"destination": "aes.zip",
@@ -38,7 +38,7 @@
"new": "23c66caa80b93199914fae246a5ea6e84db1be29"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "Arcade",
"name": "aes.zip",
"destination": "aes.zip",
@@ -47,7 +47,7 @@
"new": "ad9585c72130c56f04ae26aae87c289d"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "Arcade",
"name": "aes.zip",
"destination": "aes.zip",
@@ -56,7 +56,7 @@
"new": "12c66621"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "Arcade",
"name": "aes.zip",
"destination": "aes.zip",
@@ -889,7 +889,7 @@
}
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "Nintendo - Nintendo DSi",
"name": "dsi_bios7.bin",
"destination": "dsi_bios7.bin",
@@ -898,7 +898,7 @@
"new": "c7c7570bfe51c3c7c5da3b01331b94e7e7cb4f53"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "Nintendo - Nintendo DSi",
"name": "dsi_bios7.bin",
"destination": "dsi_bios7.bin",
@@ -907,7 +907,7 @@
"new": "322ceeafb77b0cc96a1fcc6271e0e0ae"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "Nintendo - Nintendo DSi",
"name": "dsi_bios7.bin",
"destination": "dsi_bios7.bin",
@@ -916,7 +916,7 @@
"new": "4316cc42"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "Nintendo - Nintendo DSi",
"name": "dsi_bios9.bin",
"destination": "dsi_bios9.bin",
@@ -925,7 +925,7 @@
"new": "719b9eef33692406d7170ff526069615759c4dfc"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "Nintendo - Nintendo DSi",
"name": "dsi_bios9.bin",
"destination": "dsi_bios9.bin",
@@ -934,7 +934,7 @@
"new": "23031f25602a52c2df32c39554b0c55e"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "Nintendo - Nintendo DSi",
"name": "dsi_bios9.bin",
"destination": "dsi_bios9.bin",
@@ -957,7 +957,7 @@
}
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "SNK - NeoGeo CD",
"name": "neocdz.zip",
"destination": "neocdz.zip",
@@ -1965,7 +1965,7 @@
}
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "access.dat",
"destination": "scummvm/extra/access.dat",
@@ -1974,7 +1974,7 @@
"new": "775b12b7e787bbaaeb73aa45b1ad3bb383e041ef"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "access.dat",
"destination": "scummvm/extra/access.dat",
@@ -1983,7 +1983,7 @@
"new": "4f9dd40b455876d5c54b5ebd93aec88c"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "access.dat",
"destination": "scummvm/extra/access.dat",
@@ -1992,7 +1992,7 @@
"new": "f63ddcc5"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "access.dat",
"destination": "scummvm/extra/access.dat",
@@ -2001,7 +2001,7 @@
"new": 520352
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "cryomni3d.dat",
"destination": "scummvm/extra/cryomni3d.dat",
@@ -2010,7 +2010,7 @@
"new": "3971847e26be0abad8196caf03e5852a81ddbdea"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "cryomni3d.dat",
"destination": "scummvm/extra/cryomni3d.dat",
@@ -2019,7 +2019,7 @@
"new": "7224c7a94074addeb69015b1346695d2"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "cryomni3d.dat",
"destination": "scummvm/extra/cryomni3d.dat",
@@ -2028,7 +2028,7 @@
"new": "5956e0e9"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "cryomni3d.dat",
"destination": "scummvm/extra/cryomni3d.dat",
@@ -2037,7 +2037,7 @@
"new": 70416
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "drascula.dat",
"destination": "scummvm/extra/drascula.dat",
@@ -2046,7 +2046,7 @@
"new": "05c97e92439b67aea8f6eb7fc96db023edf62124"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "drascula.dat",
"destination": "scummvm/extra/drascula.dat",
@@ -2055,7 +2055,7 @@
"new": "b304979ed642a6dcaf26f43f8a37a1de"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "drascula.dat",
"destination": "scummvm/extra/drascula.dat",
@@ -2064,7 +2064,7 @@
"new": "b36f7b0e"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "drascula.dat",
"destination": "scummvm/extra/drascula.dat",
@@ -2073,7 +2073,7 @@
"new": 259625
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "fonts.dat",
"destination": "scummvm/extra/fonts.dat",
@@ -2082,7 +2082,7 @@
"new": "1eaef39122559500fbbc705c9425500311d23e03"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "fonts.dat",
"destination": "scummvm/extra/fonts.dat",
@@ -2091,7 +2091,7 @@
"new": "6df29d218962e155d0a2114006af45c0"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "fonts.dat",
"destination": "scummvm/extra/fonts.dat",
@@ -2100,7 +2100,7 @@
"new": "f59313fa"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "fonts.dat",
"destination": "scummvm/extra/fonts.dat",
@@ -2109,7 +2109,7 @@
"new": 6449829
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "freescape.dat",
"destination": "scummvm/extra/freescape.dat",
@@ -2118,7 +2118,7 @@
"new": "c6b3d3152e4e2ff4c9bf55e103bb39ed7faca2bd"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "freescape.dat",
"destination": "scummvm/extra/freescape.dat",
@@ -2127,7 +2127,7 @@
"new": "71697f90fb0c7747616dbb0799ce966b"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "freescape.dat",
"destination": "scummvm/extra/freescape.dat",
@@ -2136,7 +2136,7 @@
"new": "54f0350e"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "freescape.dat",
"destination": "scummvm/extra/freescape.dat",
@@ -2145,7 +2145,7 @@
"new": 30124
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "hugo.dat",
"destination": "scummvm/extra/hugo.dat",
@@ -2154,7 +2154,7 @@
"new": "74a5fa172f5327fedc145cb9eccc8fd050b9cfb9"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "hugo.dat",
"destination": "scummvm/extra/hugo.dat",
@@ -2163,7 +2163,7 @@
"new": "a865e70f1b0998a500f522dab441c8fc"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "hugo.dat",
"destination": "scummvm/extra/hugo.dat",
@@ -2172,7 +2172,7 @@
"new": "fc0dd1cb"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "hugo.dat",
"destination": "scummvm/extra/hugo.dat",
@@ -2181,7 +2181,7 @@
"new": 186866
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "kyra.dat",
"destination": "scummvm/extra/kyra.dat",
@@ -2190,7 +2190,7 @@
"new": "31b0b9418af2f9dd4e4094aada05d48eb355975b"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "kyra.dat",
"destination": "scummvm/extra/kyra.dat",
@@ -2199,7 +2199,7 @@
"new": "ebddd7bbd97135a427083db5daf6f9fc"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "kyra.dat",
"destination": "scummvm/extra/kyra.dat",
@@ -2208,7 +2208,7 @@
"new": "6b7229ce"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "kyra.dat",
"destination": "scummvm/extra/kyra.dat",
@@ -2217,7 +2217,7 @@
"new": 2028520
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "lure.dat",
"destination": "scummvm/extra/lure.dat",
@@ -2226,7 +2226,7 @@
"new": "516347ec0ec2a397265d1b2fb053aa3758382e09"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "lure.dat",
"destination": "scummvm/extra/lure.dat",
@@ -2235,7 +2235,7 @@
"new": "6f90cf73f36453d332d5c9cee311f81c"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "lure.dat",
"destination": "scummvm/extra/lure.dat",
@@ -2244,7 +2244,7 @@
"new": "d616bea5"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "lure.dat",
"destination": "scummvm/extra/lure.dat",
@@ -2253,7 +2253,7 @@
"new": 768832
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "macventure.dat",
"destination": "scummvm/extra/macventure.dat",
@@ -2262,7 +2262,7 @@
"new": "e431e718e3c784532422ea2654d5fc6042a95c09"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "macventure.dat",
"destination": "scummvm/extra/macventure.dat",
@@ -2271,7 +2271,7 @@
"new": "3b1faf40e685b955372e02f54ed70263"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "macventure.dat",
"destination": "scummvm/extra/macventure.dat",
@@ -2280,7 +2280,7 @@
"new": "24a09690"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "macventure.dat",
"destination": "scummvm/extra/macventure.dat",
@@ -2289,7 +2289,7 @@
"new": 4146
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "mm.dat",
"destination": "scummvm/extra/mm.dat",
@@ -2298,7 +2298,7 @@
"new": "0816fe29b57911b28cbf774348f5492062326a85"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "mm.dat",
"destination": "scummvm/extra/mm.dat",
@@ -2307,7 +2307,7 @@
"new": "01f074a6ac34dd86fcc6d1e8090614f0"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "mm.dat",
"destination": "scummvm/extra/mm.dat",
@@ -2316,7 +2316,7 @@
"new": "f007671e"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "mm.dat",
"destination": "scummvm/extra/mm.dat",
@@ -2325,7 +2325,7 @@
"new": 280803
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "nancy.dat",
"destination": "scummvm/extra/nancy.dat",
@@ -2334,7 +2334,7 @@
"new": "7c8907d33da59afd4e4b62eee67541910e0b78d9"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "nancy.dat",
"destination": "scummvm/extra/nancy.dat",
@@ -2343,7 +2343,7 @@
"new": "eb7cfdb3f74972beb8f43196e4e44001"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "nancy.dat",
"destination": "scummvm/extra/nancy.dat",
@@ -2352,7 +2352,7 @@
"new": "d7976af8"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "nancy.dat",
"destination": "scummvm/extra/nancy.dat",
@@ -2361,7 +2361,7 @@
"new": 255042
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "queen.tbl",
"destination": "scummvm/extra/queen.tbl",
@@ -2370,7 +2370,7 @@
"new": "7bafa28dfd9b3be17430d467d8787d8204437ba0"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "queen.tbl",
"destination": "scummvm/extra/queen.tbl",
@@ -2379,7 +2379,7 @@
"new": "93c6d6379eb0290ce93854a6888ec3f4"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "queen.tbl",
"destination": "scummvm/extra/queen.tbl",
@@ -2388,7 +2388,7 @@
"new": "d713b1ab"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "queen.tbl",
"destination": "scummvm/extra/queen.tbl",
@@ -2397,7 +2397,7 @@
"new": 1100505
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "supernova.dat",
"destination": "scummvm/extra/supernova.dat",
@@ -2406,7 +2406,7 @@
"new": "8a571cfc431b5fe794f0262f0e55f72927ab27b4"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "supernova.dat",
"destination": "scummvm/extra/supernova.dat",
@@ -2415,7 +2415,7 @@
"new": "89664136c503a30894d5c713be5bfe90"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "supernova.dat",
"destination": "scummvm/extra/supernova.dat",
@@ -2424,7 +2424,7 @@
"new": "9643ad1a"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "supernova.dat",
"destination": "scummvm/extra/supernova.dat",
@@ -2433,7 +2433,7 @@
"new": 523211
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "teenagent.dat",
"destination": "scummvm/extra/teenagent.dat",
@@ -2442,7 +2442,7 @@
"new": "0d488164930332bae3aa1ad38cef44abbe3d00a6"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "teenagent.dat",
"destination": "scummvm/extra/teenagent.dat",
@@ -2451,7 +2451,7 @@
"new": "4594620989a0bfd8b120da2de7ac30de"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "teenagent.dat",
"destination": "scummvm/extra/teenagent.dat",
@@ -2460,7 +2460,7 @@
"new": "90b9e752"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "teenagent.dat",
"destination": "scummvm/extra/teenagent.dat",
@@ -2469,7 +2469,7 @@
"new": 403315
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "toon.dat",
"destination": "scummvm/extra/toon.dat",
@@ -2478,7 +2478,7 @@
"new": "85bb688125d66f2797a41f153ce1fa177027a83f"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "toon.dat",
"destination": "scummvm/extra/toon.dat",
@@ -2487,7 +2487,7 @@
"new": "2b71a5d9aa19219d46711e8a77451f0f"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "toon.dat",
"destination": "scummvm/extra/toon.dat",
@@ -2496,7 +2496,7 @@
"new": "7731190d"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "toon.dat",
"destination": "scummvm/extra/toon.dat",
@@ -2505,7 +2505,7 @@
"new": 20996
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "ultima.dat",
"destination": "scummvm/extra/ultima.dat",
@@ -2514,7 +2514,7 @@
"new": "e049932518390257f4d49b1691cad229e6060079"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "ultima.dat",
"destination": "scummvm/extra/ultima.dat",
@@ -2523,7 +2523,7 @@
"new": "e359fb7f14c4cb072b6e9300a23a6d62"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "ultima.dat",
"destination": "scummvm/extra/ultima.dat",
@@ -2532,7 +2532,7 @@
"new": "c15f9059"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "ultima.dat",
"destination": "scummvm/extra/ultima.dat",
@@ -2541,7 +2541,7 @@
"new": 15981464
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "wintermute.zip",
"destination": "scummvm/extra/wintermute.zip",
@@ -2550,7 +2550,7 @@
"new": "5302f543384b21fb55466118a6966e5ef46b85f6"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "wintermute.zip",
"destination": "scummvm/extra/wintermute.zip",
@@ -2559,7 +2559,7 @@
"new": "9783248899f9cb531f0a3e0a711ab8b3"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "wintermute.zip",
"destination": "scummvm/extra/wintermute.zip",
@@ -2568,7 +2568,7 @@
"new": "f7dc9bc4"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "wintermute.zip",
"destination": "scummvm/extra/wintermute.zip",
@@ -2577,7 +2577,7 @@
"new": 4520
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "gui-icons.dat",
"destination": "scummvm/theme/gui-icons.dat",
@@ -2586,7 +2586,7 @@
"new": "38e74e6994924781487a0d39cc53dc4708a90c1c"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "gui-icons.dat",
"destination": "scummvm/theme/gui-icons.dat",
@@ -2595,7 +2595,7 @@
"new": "9c59e0e9875434456ec1b7eb8e51666f"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "gui-icons.dat",
"destination": "scummvm/theme/gui-icons.dat",
@@ -2604,7 +2604,7 @@
"new": "0aa0c87d"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "gui-icons.dat",
"destination": "scummvm/theme/gui-icons.dat",
@@ -2613,7 +2613,7 @@
"new": 3315855
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "residualvm.zip",
"destination": "scummvm/theme/residualvm.zip",
@@ -2622,7 +2622,7 @@
"new": "589b622f55c9944c4883e4163d4d49508e22e056"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "residualvm.zip",
"destination": "scummvm/theme/residualvm.zip",
@@ -2631,7 +2631,7 @@
"new": "b2811fa9b209733b61cd49ddb21e1e95"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "residualvm.zip",
"destination": "scummvm/theme/residualvm.zip",
@@ -2640,7 +2640,7 @@
"new": "93296840"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "residualvm.zip",
"destination": "scummvm/theme/residualvm.zip",
@@ -2649,7 +2649,7 @@
"new": 94555
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "scummclassic.zip",
"destination": "scummvm/theme/scummclassic.zip",
@@ -2658,7 +2658,7 @@
"new": "69836fb392710f2182df07bab8960e8a2d92c1a9"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "scummclassic.zip",
"destination": "scummvm/theme/scummclassic.zip",
@@ -2667,7 +2667,7 @@
"new": "8f37bd19dd51be456ac8f005fd5669a8"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "scummclassic.zip",
"destination": "scummvm/theme/scummclassic.zip",
@@ -2676,7 +2676,7 @@
"new": "103168ea"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "scummclassic.zip",
"destination": "scummvm/theme/scummclassic.zip",
@@ -2685,7 +2685,7 @@
"new": 16488
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "scummmodern.zip",
"destination": "scummvm/theme/scummmodern.zip",
@@ -2694,7 +2694,7 @@
"new": "aa2dfd12c11c6e6f3baaf7bf199bd6bcd71a520c"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "scummmodern.zip",
"destination": "scummvm/theme/scummmodern.zip",
@@ -2703,7 +2703,7 @@
"new": "ee71d6cd464c618c632e6fd62b430d2c"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "scummmodern.zip",
"destination": "scummvm/theme/scummmodern.zip",
@@ -2712,7 +2712,7 @@
"new": "00194297"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "scummmodern.zip",
"destination": "scummvm/theme/scummmodern.zip",
@@ -2721,7 +2721,7 @@
"new": 68087
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "scummremastered.zip",
"destination": "scummvm/theme/scummremastered.zip",
@@ -2730,7 +2730,7 @@
"new": "03e53349eefc39ce25220bfcb1ae4a580704d29d"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "scummremastered.zip",
"destination": "scummvm/theme/scummremastered.zip",
@@ -2739,7 +2739,7 @@
"new": "0d990484d0dfc1680b20cfabcae373cd"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "scummremastered.zip",
"destination": "scummvm/theme/scummremastered.zip",
@@ -2748,7 +2748,7 @@
"new": "6fa86fae"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "scummremastered.zip",
"destination": "scummvm/theme/scummremastered.zip",
@@ -2757,7 +2757,7 @@
"new": 93901
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "translations.dat",
"destination": "scummvm/theme/translations.dat",
@@ -2766,7 +2766,7 @@
"new": "e5b6f6d6473e2d6aa0a250fd9983b573ebf6fbe4"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "translations.dat",
"destination": "scummvm/theme/translations.dat",
@@ -2775,7 +2775,7 @@
"new": "a206e54bac1af13559ac8b2d9e550577"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "translations.dat",
"destination": "scummvm/theme/translations.dat",
@@ -2784,7 +2784,7 @@
"new": "14689714"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "translations.dat",
"destination": "scummvm/theme/translations.dat",
@@ -2793,7 +2793,7 @@
"new": 3693914
}
],
"hashes_corrected": [
"hashes_updated": [
"Arcade/aes.zip sha1",
"Arcade/aes.zip md5",
"Arcade/aes.zip crc32",
@@ -2875,7 +2875,7 @@
"ScummVM/translations.dat crc32"
],
"hashes_filled": [],
"sizes_corrected": [
"sizes_updated": [
"Arcade/aes.zip",
"ScummVM/access.dat",
"ScummVM/cryomni3d.dat",
@@ -2908,7 +2908,7 @@
"SNK - NeoGeo CD/neocdz.zip sha1",
"SNK - NeoGeo CD/neocdz.zip crc32"
],
"requirements_corrected": 0,
"required_aligned": 0,
"withheld": [],
"found_by_content": 97,
"removal_candidates": [
+3 -3
View File
@@ -15,12 +15,12 @@
"not_kept": 0
},
"changes": [],
"hashes_corrected": [],
"hashes_updated": [],
"hashes_filled": [],
"sizes_corrected": [],
"sizes_updated": [],
"sizes_removed": [],
"hashes_cleared": [],
"requirements_corrected": 0,
"required_aligned": 0,
"withheld": [],
"found_by_content": 0,
"removal_candidates": [],
+133 -133
View File
File diff suppressed because it is too large. Load diff
+106 -106
View File
@@ -29,7 +29,7 @@
}
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "Arcade",
"name": "aes.zip",
"destination": "aes.zip",
@@ -38,7 +38,7 @@
"new": "23c66caa80b93199914fae246a5ea6e84db1be29"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "Arcade",
"name": "aes.zip",
"destination": "aes.zip",
@@ -47,7 +47,7 @@
"new": "ad9585c72130c56f04ae26aae87c289d"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "Arcade",
"name": "aes.zip",
"destination": "aes.zip",
@@ -56,7 +56,7 @@
"new": "12c66621"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "Arcade",
"name": "aes.zip",
"destination": "aes.zip",
@@ -903,7 +903,7 @@
}
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "Nintendo - Nintendo DSi",
"name": "dsi_bios7.bin",
"destination": "dsi_bios7.bin",
@@ -912,7 +912,7 @@
"new": "c7c7570bfe51c3c7c5da3b01331b94e7e7cb4f53"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "Nintendo - Nintendo DSi",
"name": "dsi_bios7.bin",
"destination": "dsi_bios7.bin",
@@ -921,7 +921,7 @@
"new": "322ceeafb77b0cc96a1fcc6271e0e0ae"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "Nintendo - Nintendo DSi",
"name": "dsi_bios7.bin",
"destination": "dsi_bios7.bin",
@@ -930,7 +930,7 @@
"new": "4316cc42"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "Nintendo - Nintendo DSi",
"name": "dsi_bios9.bin",
"destination": "dsi_bios9.bin",
@@ -939,7 +939,7 @@
"new": "719b9eef33692406d7170ff526069615759c4dfc"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "Nintendo - Nintendo DSi",
"name": "dsi_bios9.bin",
"destination": "dsi_bios9.bin",
@@ -948,7 +948,7 @@
"new": "23031f25602a52c2df32c39554b0c55e"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "Nintendo - Nintendo DSi",
"name": "dsi_bios9.bin",
"destination": "dsi_bios9.bin",
@@ -971,7 +971,7 @@
}
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "SNK - NeoGeo CD",
"name": "neocdz.zip",
"destination": "neocdz.zip",
@@ -1979,7 +1979,7 @@
}
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "access.dat",
"destination": "scummvm/extra/access.dat",
@@ -1988,7 +1988,7 @@
"new": "775b12b7e787bbaaeb73aa45b1ad3bb383e041ef"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "access.dat",
"destination": "scummvm/extra/access.dat",
@@ -1997,7 +1997,7 @@
"new": "4f9dd40b455876d5c54b5ebd93aec88c"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "access.dat",
"destination": "scummvm/extra/access.dat",
@@ -2006,7 +2006,7 @@
"new": "f63ddcc5"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "access.dat",
"destination": "scummvm/extra/access.dat",
@@ -2015,7 +2015,7 @@
"new": 520352
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "cryomni3d.dat",
"destination": "scummvm/extra/cryomni3d.dat",
@@ -2024,7 +2024,7 @@
"new": "3971847e26be0abad8196caf03e5852a81ddbdea"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "cryomni3d.dat",
"destination": "scummvm/extra/cryomni3d.dat",
@@ -2033,7 +2033,7 @@
"new": "7224c7a94074addeb69015b1346695d2"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "cryomni3d.dat",
"destination": "scummvm/extra/cryomni3d.dat",
@@ -2042,7 +2042,7 @@
"new": "5956e0e9"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "cryomni3d.dat",
"destination": "scummvm/extra/cryomni3d.dat",
@@ -2051,7 +2051,7 @@
"new": 70416
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "drascula.dat",
"destination": "scummvm/extra/drascula.dat",
@@ -2060,7 +2060,7 @@
"new": "05c97e92439b67aea8f6eb7fc96db023edf62124"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "drascula.dat",
"destination": "scummvm/extra/drascula.dat",
@@ -2069,7 +2069,7 @@
"new": "b304979ed642a6dcaf26f43f8a37a1de"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "drascula.dat",
"destination": "scummvm/extra/drascula.dat",
@@ -2078,7 +2078,7 @@
"new": "b36f7b0e"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "drascula.dat",
"destination": "scummvm/extra/drascula.dat",
@@ -2087,7 +2087,7 @@
"new": 259625
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "fonts.dat",
"destination": "scummvm/extra/fonts.dat",
@@ -2096,7 +2096,7 @@
"new": "1eaef39122559500fbbc705c9425500311d23e03"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "fonts.dat",
"destination": "scummvm/extra/fonts.dat",
@@ -2105,7 +2105,7 @@
"new": "6df29d218962e155d0a2114006af45c0"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "fonts.dat",
"destination": "scummvm/extra/fonts.dat",
@@ -2114,7 +2114,7 @@
"new": "f59313fa"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "fonts.dat",
"destination": "scummvm/extra/fonts.dat",
@@ -2123,7 +2123,7 @@
"new": 6449829
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "freescape.dat",
"destination": "scummvm/extra/freescape.dat",
@@ -2132,7 +2132,7 @@
"new": "c6b3d3152e4e2ff4c9bf55e103bb39ed7faca2bd"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "freescape.dat",
"destination": "scummvm/extra/freescape.dat",
@@ -2141,7 +2141,7 @@
"new": "71697f90fb0c7747616dbb0799ce966b"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "freescape.dat",
"destination": "scummvm/extra/freescape.dat",
@@ -2150,7 +2150,7 @@
"new": "54f0350e"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "freescape.dat",
"destination": "scummvm/extra/freescape.dat",
@@ -2159,7 +2159,7 @@
"new": 30124
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "hugo.dat",
"destination": "scummvm/extra/hugo.dat",
@@ -2168,7 +2168,7 @@
"new": "74a5fa172f5327fedc145cb9eccc8fd050b9cfb9"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "hugo.dat",
"destination": "scummvm/extra/hugo.dat",
@@ -2177,7 +2177,7 @@
"new": "a865e70f1b0998a500f522dab441c8fc"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "hugo.dat",
"destination": "scummvm/extra/hugo.dat",
@@ -2186,7 +2186,7 @@
"new": "fc0dd1cb"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "hugo.dat",
"destination": "scummvm/extra/hugo.dat",
@@ -2195,7 +2195,7 @@
"new": 186866
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "kyra.dat",
"destination": "scummvm/extra/kyra.dat",
@@ -2204,7 +2204,7 @@
"new": "31b0b9418af2f9dd4e4094aada05d48eb355975b"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "kyra.dat",
"destination": "scummvm/extra/kyra.dat",
@@ -2213,7 +2213,7 @@
"new": "ebddd7bbd97135a427083db5daf6f9fc"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "kyra.dat",
"destination": "scummvm/extra/kyra.dat",
@@ -2222,7 +2222,7 @@
"new": "6b7229ce"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "kyra.dat",
"destination": "scummvm/extra/kyra.dat",
@@ -2231,7 +2231,7 @@
"new": 2028520
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "lure.dat",
"destination": "scummvm/extra/lure.dat",
@@ -2240,7 +2240,7 @@
"new": "516347ec0ec2a397265d1b2fb053aa3758382e09"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "lure.dat",
"destination": "scummvm/extra/lure.dat",
@@ -2249,7 +2249,7 @@
"new": "6f90cf73f36453d332d5c9cee311f81c"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "lure.dat",
"destination": "scummvm/extra/lure.dat",
@@ -2258,7 +2258,7 @@
"new": "d616bea5"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "lure.dat",
"destination": "scummvm/extra/lure.dat",
@@ -2267,7 +2267,7 @@
"new": 768832
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "macventure.dat",
"destination": "scummvm/extra/macventure.dat",
@@ -2276,7 +2276,7 @@
"new": "e431e718e3c784532422ea2654d5fc6042a95c09"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "macventure.dat",
"destination": "scummvm/extra/macventure.dat",
@@ -2285,7 +2285,7 @@
"new": "3b1faf40e685b955372e02f54ed70263"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "macventure.dat",
"destination": "scummvm/extra/macventure.dat",
@@ -2294,7 +2294,7 @@
"new": "24a09690"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "macventure.dat",
"destination": "scummvm/extra/macventure.dat",
@@ -2303,7 +2303,7 @@
"new": 4146
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "mm.dat",
"destination": "scummvm/extra/mm.dat",
@@ -2312,7 +2312,7 @@
"new": "0816fe29b57911b28cbf774348f5492062326a85"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "mm.dat",
"destination": "scummvm/extra/mm.dat",
@@ -2321,7 +2321,7 @@
"new": "01f074a6ac34dd86fcc6d1e8090614f0"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "mm.dat",
"destination": "scummvm/extra/mm.dat",
@@ -2330,7 +2330,7 @@
"new": "f007671e"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "mm.dat",
"destination": "scummvm/extra/mm.dat",
@@ -2339,7 +2339,7 @@
"new": 280803
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "nancy.dat",
"destination": "scummvm/extra/nancy.dat",
@@ -2348,7 +2348,7 @@
"new": "7c8907d33da59afd4e4b62eee67541910e0b78d9"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "nancy.dat",
"destination": "scummvm/extra/nancy.dat",
@@ -2357,7 +2357,7 @@
"new": "eb7cfdb3f74972beb8f43196e4e44001"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "nancy.dat",
"destination": "scummvm/extra/nancy.dat",
@@ -2366,7 +2366,7 @@
"new": "d7976af8"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "nancy.dat",
"destination": "scummvm/extra/nancy.dat",
@@ -2375,7 +2375,7 @@
"new": 255042
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "queen.tbl",
"destination": "scummvm/extra/queen.tbl",
@@ -2384,7 +2384,7 @@
"new": "7bafa28dfd9b3be17430d467d8787d8204437ba0"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "queen.tbl",
"destination": "scummvm/extra/queen.tbl",
@@ -2393,7 +2393,7 @@
"new": "93c6d6379eb0290ce93854a6888ec3f4"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "queen.tbl",
"destination": "scummvm/extra/queen.tbl",
@@ -2402,7 +2402,7 @@
"new": "d713b1ab"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "queen.tbl",
"destination": "scummvm/extra/queen.tbl",
@@ -2411,7 +2411,7 @@
"new": 1100505
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "supernova.dat",
"destination": "scummvm/extra/supernova.dat",
@@ -2420,7 +2420,7 @@
"new": "8a571cfc431b5fe794f0262f0e55f72927ab27b4"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "supernova.dat",
"destination": "scummvm/extra/supernova.dat",
@@ -2429,7 +2429,7 @@
"new": "89664136c503a30894d5c713be5bfe90"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "supernova.dat",
"destination": "scummvm/extra/supernova.dat",
@@ -2438,7 +2438,7 @@
"new": "9643ad1a"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "supernova.dat",
"destination": "scummvm/extra/supernova.dat",
@@ -2447,7 +2447,7 @@
"new": 523211
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "teenagent.dat",
"destination": "scummvm/extra/teenagent.dat",
@@ -2456,7 +2456,7 @@
"new": "0d488164930332bae3aa1ad38cef44abbe3d00a6"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "teenagent.dat",
"destination": "scummvm/extra/teenagent.dat",
@@ -2465,7 +2465,7 @@
"new": "4594620989a0bfd8b120da2de7ac30de"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "teenagent.dat",
"destination": "scummvm/extra/teenagent.dat",
@@ -2474,7 +2474,7 @@
"new": "90b9e752"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "teenagent.dat",
"destination": "scummvm/extra/teenagent.dat",
@@ -2483,7 +2483,7 @@
"new": 403315
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "toon.dat",
"destination": "scummvm/extra/toon.dat",
@@ -2492,7 +2492,7 @@
"new": "85bb688125d66f2797a41f153ce1fa177027a83f"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "toon.dat",
"destination": "scummvm/extra/toon.dat",
@@ -2501,7 +2501,7 @@
"new": "2b71a5d9aa19219d46711e8a77451f0f"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "toon.dat",
"destination": "scummvm/extra/toon.dat",
@@ -2510,7 +2510,7 @@
"new": "7731190d"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "toon.dat",
"destination": "scummvm/extra/toon.dat",
@@ -2519,7 +2519,7 @@
"new": 20996
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "ultima.dat",
"destination": "scummvm/extra/ultima.dat",
@@ -2528,7 +2528,7 @@
"new": "e049932518390257f4d49b1691cad229e6060079"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "ultima.dat",
"destination": "scummvm/extra/ultima.dat",
@@ -2537,7 +2537,7 @@
"new": "e359fb7f14c4cb072b6e9300a23a6d62"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "ultima.dat",
"destination": "scummvm/extra/ultima.dat",
@@ -2546,7 +2546,7 @@
"new": "c15f9059"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "ultima.dat",
"destination": "scummvm/extra/ultima.dat",
@@ -2555,7 +2555,7 @@
"new": 15981464
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "wintermute.zip",
"destination": "scummvm/extra/wintermute.zip",
@@ -2564,7 +2564,7 @@
"new": "5302f543384b21fb55466118a6966e5ef46b85f6"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "wintermute.zip",
"destination": "scummvm/extra/wintermute.zip",
@@ -2573,7 +2573,7 @@
"new": "9783248899f9cb531f0a3e0a711ab8b3"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "wintermute.zip",
"destination": "scummvm/extra/wintermute.zip",
@@ -2582,7 +2582,7 @@
"new": "f7dc9bc4"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "wintermute.zip",
"destination": "scummvm/extra/wintermute.zip",
@@ -2591,7 +2591,7 @@
"new": 4520
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "gui-icons.dat",
"destination": "scummvm/theme/gui-icons.dat",
@@ -2600,7 +2600,7 @@
"new": "38e74e6994924781487a0d39cc53dc4708a90c1c"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "gui-icons.dat",
"destination": "scummvm/theme/gui-icons.dat",
@@ -2609,7 +2609,7 @@
"new": "9c59e0e9875434456ec1b7eb8e51666f"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "gui-icons.dat",
"destination": "scummvm/theme/gui-icons.dat",
@@ -2618,7 +2618,7 @@
"new": "0aa0c87d"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "gui-icons.dat",
"destination": "scummvm/theme/gui-icons.dat",
@@ -2627,7 +2627,7 @@
"new": 3315855
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "residualvm.zip",
"destination": "scummvm/theme/residualvm.zip",
@@ -2636,7 +2636,7 @@
"new": "589b622f55c9944c4883e4163d4d49508e22e056"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "residualvm.zip",
"destination": "scummvm/theme/residualvm.zip",
@@ -2645,7 +2645,7 @@
"new": "b2811fa9b209733b61cd49ddb21e1e95"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "residualvm.zip",
"destination": "scummvm/theme/residualvm.zip",
@@ -2654,7 +2654,7 @@
"new": "93296840"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "residualvm.zip",
"destination": "scummvm/theme/residualvm.zip",
@@ -2663,7 +2663,7 @@
"new": 94555
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "scummclassic.zip",
"destination": "scummvm/theme/scummclassic.zip",
@@ -2672,7 +2672,7 @@
"new": "69836fb392710f2182df07bab8960e8a2d92c1a9"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "scummclassic.zip",
"destination": "scummvm/theme/scummclassic.zip",
@@ -2681,7 +2681,7 @@
"new": "8f37bd19dd51be456ac8f005fd5669a8"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "scummclassic.zip",
"destination": "scummvm/theme/scummclassic.zip",
@@ -2690,7 +2690,7 @@
"new": "103168ea"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "scummclassic.zip",
"destination": "scummvm/theme/scummclassic.zip",
@@ -2699,7 +2699,7 @@
"new": 16488
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "scummmodern.zip",
"destination": "scummvm/theme/scummmodern.zip",
@@ -2708,7 +2708,7 @@
"new": "aa2dfd12c11c6e6f3baaf7bf199bd6bcd71a520c"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "scummmodern.zip",
"destination": "scummvm/theme/scummmodern.zip",
@@ -2717,7 +2717,7 @@
"new": "ee71d6cd464c618c632e6fd62b430d2c"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "scummmodern.zip",
"destination": "scummvm/theme/scummmodern.zip",
@@ -2726,7 +2726,7 @@
"new": "00194297"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "scummmodern.zip",
"destination": "scummvm/theme/scummmodern.zip",
@@ -2735,7 +2735,7 @@
"new": 68087
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "scummremastered.zip",
"destination": "scummvm/theme/scummremastered.zip",
@@ -2744,7 +2744,7 @@
"new": "03e53349eefc39ce25220bfcb1ae4a580704d29d"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "scummremastered.zip",
"destination": "scummvm/theme/scummremastered.zip",
@@ -2753,7 +2753,7 @@
"new": "0d990484d0dfc1680b20cfabcae373cd"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "scummremastered.zip",
"destination": "scummvm/theme/scummremastered.zip",
@@ -2762,7 +2762,7 @@
"new": "6fa86fae"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "scummremastered.zip",
"destination": "scummvm/theme/scummremastered.zip",
@@ -2771,7 +2771,7 @@
"new": 93901
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "translations.dat",
"destination": "scummvm/theme/translations.dat",
@@ -2780,7 +2780,7 @@
"new": "e5b6f6d6473e2d6aa0a250fd9983b573ebf6fbe4"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "translations.dat",
"destination": "scummvm/theme/translations.dat",
@@ -2789,7 +2789,7 @@
"new": "a206e54bac1af13559ac8b2d9e550577"
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "ScummVM",
"name": "translations.dat",
"destination": "scummvm/theme/translations.dat",
@@ -2798,7 +2798,7 @@
"new": "14689714"
},
{
"kind": "size_corrected",
"kind": "size_updated",
"system": "ScummVM",
"name": "translations.dat",
"destination": "scummvm/theme/translations.dat",
@@ -2947,7 +2947,7 @@
}
}
],
"hashes_corrected": [
"hashes_updated": [
"Arcade/aes.zip sha1",
"Arcade/aes.zip md5",
"Arcade/aes.zip crc32",
@@ -3029,7 +3029,7 @@
"ScummVM/translations.dat crc32"
],
"hashes_filled": [],
"sizes_corrected": [
"sizes_updated": [
"Arcade/aes.zip",
"ScummVM/access.dat",
"ScummVM/cryomni3d.dat",
@@ -3062,7 +3062,7 @@
"SNK - NeoGeo CD/neocdz.zip sha1",
"SNK - NeoGeo CD/neocdz.zip crc32"
],
"requirements_corrected": 0,
"required_aligned": 0,
"withheld": [],
"found_by_content": 97,
"removal_candidates": [
+5 -5
View File
@@ -662,7 +662,7 @@
}
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "dragon32",
"name": "d64_1.rom",
"destination": "dragon/d64_1.rom",
@@ -766,7 +766,7 @@
}
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "neogeocd",
"name": "neocdz.zip",
"destination": "neocdz.zip",
@@ -885,7 +885,7 @@
}
}
],
"hashes_corrected": [
"hashes_updated": [
"dragon32/d64_1.rom md5",
"neogeocd/neocdz.zip md5"
],
@@ -894,10 +894,10 @@
"nds/dsi_bios9.bin md5",
"nds/dsi_bios7.bin md5"
],
"sizes_corrected": [],
"sizes_updated": [],
"sizes_removed": [],
"hashes_cleared": [],
"requirements_corrected": 0,
"required_aligned": 0,
"withheld": [],
"found_by_content": 420,
"removal_candidates": [
+56 -56
View File
@@ -379,7 +379,7 @@
"new": "bf05cc848e403f8ae457bfe26faa8135cf4725cfff956fa030f5c0a00a26adcc"
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "mame",
"name": "p1_rom.zip",
"destination": "bios/p1_rom.zip",
@@ -388,7 +388,7 @@
"new": false
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "fbneo",
"name": "neocdz.zip",
"destination": "bios/neocdz.zip",
@@ -408,7 +408,7 @@
}
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "nds",
"name": "bios7.bin",
"destination": "bios/bios7.bin",
@@ -417,7 +417,7 @@
"new": false
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "nds",
"name": "bios9.bin",
"destination": "bios/bios9.bin",
@@ -503,7 +503,7 @@
}
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "pico8",
"name": "pico8_dyn",
"destination": "bios/pico-8/pico8_dyn",
@@ -649,7 +649,7 @@
}
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "amiga",
"name": "kick34005.A500",
"destination": "bios/kick34005.A500",
@@ -658,7 +658,7 @@
"new": true
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "amiga",
"name": "kick40068.A1200",
"destination": "bios/kick40068.A1200",
@@ -667,7 +667,7 @@
"new": true
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "amiga",
"name": "kick34005.CDTV",
"destination": "bios/kick34005.CDTV",
@@ -676,7 +676,7 @@
"new": true
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "amiga",
"name": "kick40060.CD32",
"destination": "bios/kick40060.CD32",
@@ -685,7 +685,7 @@
"new": true
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "amiga",
"name": "kick40060.CD32.ext",
"destination": "bios/kick40060.CD32.ext",
@@ -730,7 +730,7 @@
"new": "4b5ed8cb2472013c19021fea99352fee6a50a116295cc38d4c040514c2eb39f1"
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "atarixe",
"name": "xegs.zip",
"destination": "bios/xegs.zip",
@@ -739,7 +739,7 @@
"new": true
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "atari5200",
"name": "ATARIXL.ROM",
"destination": "bios/ATARIXL.ROM",
@@ -748,7 +748,7 @@
"new": false
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "atari5200",
"name": "ATARIBAS.ROM",
"destination": "bios/ATARIBAS.ROM",
@@ -757,7 +757,7 @@
"new": false
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "atari5200",
"name": "ATARIOSA.ROM",
"destination": "bios/ATARIOSA.ROM",
@@ -953,7 +953,7 @@
}
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "apple2",
"name": "a2diskiing.zip",
"destination": "bios/a2diskiing.zip",
@@ -962,7 +962,7 @@
"new": true
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "apple2",
"name": "apple2e.zip",
"destination": "bios/apple2e.zip",
@@ -971,7 +971,7 @@
"new": true
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "apple2",
"name": "d2fdc.zip",
"destination": "bios/d2fdc.zip",
@@ -980,7 +980,7 @@
"new": true
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "apple2",
"name": "apple2gs.zip",
"destination": "bios/apple2gs.zip",
@@ -989,7 +989,7 @@
"new": true
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "cdimono1",
"name": "cdibios.zip",
"destination": "bios/cdibios.zip",
@@ -1186,7 +1186,7 @@
}
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "gamecom",
"name": "gamecom.zip",
"destination": "bios/gamecom.zip",
@@ -1206,7 +1206,7 @@
}
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "gmaster",
"name": "gmaster.zip",
"destination": "bios/gmaster.zip",
@@ -1226,7 +1226,7 @@
}
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "neogeo",
"name": "aes.zip",
"destination": "bios/aes.zip",
@@ -1253,7 +1253,7 @@
"new": "1d39368f8dd5c2c65be1b872e749f7e20fe5707f9897deefe54a82d11d4748c7"
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "ngp",
"name": "ngp.zip",
"destination": "bios/ngp.zip",
@@ -1262,7 +1262,7 @@
"new": true
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "odyssey2",
"name": "c52.bin",
"destination": "bios/c52.bin",
@@ -1271,7 +1271,7 @@
"new": false
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "msx",
"name": "DISK.ROM",
"destination": "bios/DISK.ROM",
@@ -1289,7 +1289,7 @@
"new": "31740a761447d7590c304338d389b0ae114546a8b9f79c9ffd5e374f80b7d354"
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "msx",
"name": "MSXDOS2.ROM",
"destination": "bios/MSXDOS2.ROM",
@@ -1325,7 +1325,7 @@
"new": "56785703b14f252ce77be586716dbf5d94125e93b9e6069ff26f7686566cd91a"
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "msx",
"name": "CARTS.SHA",
"destination": "bios/CARTS.SHA",
@@ -1343,7 +1343,7 @@
"new": "0a09940701f40647a2715271b43c9ab0d82bd488871427ad627b7e0f462f5dda"
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "msx",
"name": "RS232.ROM",
"destination": "bios/RS232.ROM",
@@ -1415,7 +1415,7 @@
"new": "728fbc6dd5f7532b526d1fa6e55bade10060954cf5412d23e61ceafaa1e6bf85"
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "nes",
"name": "disksys.rom",
"destination": "bios/disksys.rom",
@@ -1424,7 +1424,7 @@
"new": true
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "sega32x",
"name": "32X_M_BIOS.BIN",
"destination": "bios/32X_M_BIOS.BIN",
@@ -1433,7 +1433,7 @@
"new": true
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "sega32x",
"name": "32X_S_BIOS.BIN",
"destination": "bios/32X_S_BIOS.BIN",
@@ -1442,7 +1442,7 @@
"new": true
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "dreamcast",
"name": "dc_boot.bin",
"destination": "bios/dc/dc_boot.bin",
@@ -1451,7 +1451,7 @@
"new": false
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "dreamcast",
"name": "dc_flash.bin",
"destination": "bios/dc/dc_flash.bin",
@@ -1478,7 +1478,7 @@
"new": "8c8a21335038285cfa03dc076100c1f0bfadf3e4ff70796f11f3dfaaab60eee2"
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "colecovision",
"name": "colecovision.rom",
"destination": "bios/colecovision.rom",
@@ -1487,7 +1487,7 @@
"new": true
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "colecovision",
"name": "coleco.rom",
"destination": "bios/coleco.rom",
@@ -1677,7 +1677,7 @@
}
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "snes",
"name": "BS-X.bin",
"destination": "bios/BS-X.bin",
@@ -1686,7 +1686,7 @@
"new": true
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "snes",
"name": "STBIOS.bin",
"destination": "bios/STBIOS.bin",
@@ -1731,7 +1731,7 @@
"new": "ea65e0fb44ee93ede4b6c507509b7e5ddf497fb7155023bea91ef229469fa04d"
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "scv",
"name": "upd7801g.s01",
"destination": "bios/upd7801g.s01",
@@ -1749,7 +1749,7 @@
"new": "248ec2f28294316558475ba0e416da19cfff0a5dd8ad3525a484a9be67b8a290"
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "scv",
"name": "scv.zip",
"destination": "bios/scv.zip",
@@ -1769,7 +1769,7 @@
}
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "supracan",
"name": "umc6650.zip",
"destination": "bios/umc6650.zip",
@@ -1778,7 +1778,7 @@
"new": true
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "supracan",
"name": "supracan.zip",
"destination": "bios/supracan.zip",
@@ -1809,7 +1809,7 @@
}
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "vsmile",
"name": "vsmile.zip",
"destination": "bios/vsmile.zip",
@@ -1840,7 +1840,7 @@
}
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "xbox",
"name": "mcpx_1.0.bin",
"destination": "bios/mcpx_1.0.bin",
@@ -1849,7 +1849,7 @@
"new": true
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "xbox",
"name": "Complex.bin",
"destination": "bios/Complex.bin",
@@ -1878,7 +1878,7 @@
"new": "fc0e900bfec6b52f0f80ba1e65a4712808d2a411b5b00496639ef1a2152351f1"
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "tanodragon",
"name": "d64tano.rom",
"destination": "bios/d64tano.rom",
@@ -1905,7 +1905,7 @@
"new": "c3d0b8fd92020c939ec97388402d10b3890e6e26455963d50cc00b5f9cd68fa5"
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "tanodragon",
"name": "d200rom1.rom",
"destination": "bios/d200rom1.rom",
@@ -1923,7 +1923,7 @@
"new": "0486c51d3b6f7b6b4bcdde661d54f076278fd6a4ee92cbf9d1bc791554e16bd7"
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "tanodragon",
"name": "ddos10.rom",
"destination": "bios/ddos10.rom",
@@ -1986,7 +1986,7 @@
"new": "09e8573e31effc16e9f1a177f46514ed9f332a6c5e4e46a550574bd969319c11"
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "tanodragon",
"name": "cp400bas.rom",
"destination": "bios/cp400bas.rom",
@@ -2069,7 +2069,7 @@
}
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "coco",
"name": "cp400extbas.rom",
"destination": "bios/cp400extbas.rom",
@@ -2123,7 +2123,7 @@
"new": "5375b1caef9df815b781d9cd6df81ecdaddd2642b06a01adac521e40f1329751"
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "coco",
"name": "coco3.rom",
"destination": "bios/coco3.rom",
@@ -2150,7 +2150,7 @@
"new": "ccd347508f177cb3eeb2e17fd60d3cccb64312d069c5084771467cfbbc04a27c"
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "coco",
"name": "extbas11.rom",
"destination": "bios/extbas11.rom",
@@ -2177,7 +2177,7 @@
"new": "8c86c83720043525029797dd3defd0fd285651549a373117304cc470e8e61f91"
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "coco",
"name": "disk11.rom",
"destination": "bios/disk11.rom",
@@ -2195,7 +2195,7 @@
"new": "1c40c5a74e72edaeecc2511fee1aab347eae2c3d5fc6778cb622e3df14d63157"
},
{
"kind": "requirement_corrected",
"kind": "required_aligned",
"system": "coco",
"name": "coco3p.rom",
"destination": "bios/coco3p.rom",
@@ -2265,7 +2265,7 @@
}
}
],
"hashes_corrected": [
"hashes_updated": [
"fbneo/neocdz.zip md5"
],
"hashes_filled": [
@@ -2364,10 +2364,10 @@
"coco/disk11.rom sha256",
"coco/coco3p.rom sha256"
],
"sizes_corrected": [],
"sizes_updated": [],
"sizes_removed": [],
"hashes_cleared": [],
"requirements_corrected": 52,
"required_aligned": 52,
"withheld": [],
"found_by_content": 279,
"removal_candidates": [
+3 -3
View File
@@ -40,12 +40,12 @@
"new": {}
}
],
"hashes_corrected": [],
"hashes_updated": [],
"hashes_filled": [],
"sizes_corrected": [],
"sizes_updated": [],
"sizes_removed": [],
"hashes_cleared": [],
"requirements_corrected": 0,
"required_aligned": 0,
"withheld": [],
"found_by_content": 98,
"removal_candidates": [
+3 -3
View File
@@ -27,12 +27,12 @@
}
}
],
"hashes_corrected": [],
"hashes_updated": [],
"hashes_filled": [],
"sizes_corrected": [],
"sizes_updated": [],
"sizes_removed": [],
"hashes_cleared": [],
"requirements_corrected": 0,
"required_aligned": 0,
"withheld": [],
"found_by_content": 132,
"removal_candidates": [
+4 -4
View File
@@ -1316,7 +1316,7 @@
}
},
{
"kind": "hash_corrected",
"kind": "hash_updated",
"system": "doom",
"name": "PLUTONIA.WAD",
"destination": "doom/PLUTONIA.WAD",
@@ -1325,14 +1325,14 @@
"new": "816c7c6b0098f66c299c9253f62bd908456efb63"
}
],
"hashes_corrected": [
"hashes_updated": [
"doom/PLUTONIA.WAD sha1"
],
"hashes_filled": [],
"sizes_corrected": [],
"sizes_updated": [],
"sizes_removed": [],
"hashes_cleared": [],
"requirements_corrected": 0,
"required_aligned": 0,
"withheld": [],
"found_by_content": 0,
"removal_candidates": [
+21 -26
View File
@@ -6,7 +6,8 @@ Pipeline:
2. Scan old branches (git show origin/branch:path)
3. Search public BIOS repos on GitHub
4. Search archive.org collections
5. Create GitHub Issue for community help
5. Print an issue body for the files still missing (--create-issues).
It posts nothing.
Usage:
python scripts/auto_fetch.py --platform retroarch [--dry-run]
@@ -459,43 +460,37 @@ def fetch_missing(
)
stats["errors"] += 1
continue
print(" [5] Not found - needs community contribution")
print(" [5] Not found, open for contributions")
still_missing.append(entry)
stats["not_found"] += 1
return {"stats": stats, "still_missing": still_missing}
CONTRIBUTING_URL = "https://github.com/Abdess/retrobios/blob/main/CONTRIBUTING.md"
def generate_issue_body(missing: list[dict], platform: str) -> str:
"""Generate a GitHub Issue body for missing BIOS files."""
"""An issue body for the files a platform still needs.
The hashes are written in full: a contributor matches a file against
them, and a 12-character prefix confirms nothing.
"""
lines = [
f"## Missing BIOS Files for {platform}",
"",
"The following BIOS files are required but not available in the repository.",
"If you have any of these files, please submit a Pull Request!",
f"## Files {platform} still needs",
"",
"| File | System | SHA1 | MD5 |",
"|------|--------|------|-----|",
]
for entry in missing:
sha1 = ", ".join(sorted(_accepted(entry, "sha1"))) or "N/A"
md5 = ", ".join(sorted(_accepted(entry, "md5"))) or "N/A"
lines.append(
f"| `{entry['name']}` | {entry['system']} | `{sha1[:12]}...` | `{md5[:12]}...` |"
)
lines.extend(
[
"",
"### How to Contribute",
"",
"1. Fork this repository",
"2. Add the BIOS file to `bios/Manufacturer/Console/`",
"3. Create a Pull Request - checksums are verified automatically",
]
)
sha1 = ", ".join(f"`{h}`" for h in sorted(_accepted(entry, "sha1"))) or "none declared"
md5 = ", ".join(f"`{h}`" for h in sorted(_accepted(entry, "md5"))) or "none declared"
lines.append(f"| `{entry['name']}` | {entry['system']} | {sha1} | {md5} |")
lines += [
"",
"A file with one of these hashes can be added through the upload link in"
f" [CONTRIBUTING.md]({CONTRIBUTING_URL}). CI checks it on arrival.",
]
return "\n".join(lines)
@@ -563,7 +558,7 @@ def main() -> None:
result = fetch_missing(missing, db, args.bios_dir, args.dry_run)
if result["still_missing"]:
all_still_missing[platform] = result["still_missing"]
all_still_missing[config.get("platform") or platform] = result["still_missing"]
stats = result["stats"]
print(
+6 -4
View File
@@ -10,6 +10,7 @@ Usage as a hook: python scripts/commit_rules.py MESSAGE_FILE
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
@@ -51,10 +52,11 @@ def problems(message: str) -> list[str]:
def main(argv: list[str]) -> int:
if len(argv) != 2:
print("usage: commit_rules.py MESSAGE_FILE", file=sys.stderr)
return 2
found = problems(Path(argv[1]).read_text(encoding="utf-8", errors="replace"))
parser = argparse.ArgumentParser(
allow_abbrev=False, description="Check a commit message against the rules above")
parser.add_argument("message_file", type=Path, help="the message file a commit-msg hook receives")
args = parser.parse_args(argv[1:])
found = problems(args.message_file.read_text(encoding="utf-8", errors="replace"))
for problem in found:
print(f"commit-msg: {problem}", file=sys.stderr)
return 1 if found else 0
+11 -11
View File
@@ -515,7 +515,7 @@ class LandedChanges:
@property
def applied(self) -> list[str]:
return self._labels("hash_corrected")
return self._labels("hash_updated")
@property
def filled(self) -> list[str]:
@@ -527,7 +527,7 @@ class LandedChanges:
@property
def resized(self) -> list[str]:
return [label.removesuffix(" size") for label in self._labels("size_corrected")]
return [label.removesuffix(" size") for label in self._labels("size_updated")]
@property
def unsized(self) -> list[str]:
@@ -535,7 +535,7 @@ class LandedChanges:
@property
def requirements(self) -> int:
return sum(r["kind"] == "requirement_corrected" for r in self.records)
return sum(r["kind"] == "required_aligned" for r in self.records)
@property
def lists(self) -> int:
@@ -624,14 +624,14 @@ def landed_changes(exporter: object, systems: dict) -> LandedChanges:
and exporter.required_value(entry) != bool(entry.platform["required"])
):
records.append(_change(
"requirement_corrected", entry, "required",
"required_aligned", entry, "required",
bool(entry.platform["required"]), exporter.required_value(entry),
))
for field_name in entry.corrections:
if field_name == "required" or not exporter.states(entry, field_name):
continue
records.append(_change(
"hash_corrected", entry, field_name,
"hash_updated", entry, field_name,
_platform_value(entry, field_name),
_written_value(exporter, entry, field_name),
))
@@ -648,7 +648,7 @@ def landed_changes(exporter: object, systems: dict) -> LandedChanges:
and exporter.states(entry, field_name)
):
records.append(_change(
"hash_corrected", entry, field_name,
"hash_updated", entry, field_name,
_platform_value(entry, field_name), listed,
))
records.extend(
@@ -666,7 +666,7 @@ def landed_changes(exporter: object, systems: dict) -> LandedChanges:
and exporter.states(entry, "size")
):
records.append(_change(
"size_corrected", entry, "size", _single_size(theirs), ours
"size_updated", entry, "size", _single_size(theirs), ours
))
elif (
theirs
@@ -690,7 +690,7 @@ def landed_changes(exporter: object, systems: dict) -> LandedChanges:
# A value the render rewrote though the model reads it as the
# platform's own (RomM's sha1 with a leading space).
records.extend(
_change("hash_corrected", entry, field_name, old, new)
_change("hash_updated", entry, field_name, old, new)
for entry, field_name, old, new in getattr(exporter, "normalized_values", [])
)
return changes
@@ -863,12 +863,12 @@ def export_platform(
"not_kept": lost,
},
"changes": changes.records,
"hashes_corrected": applied,
"hashes_updated": applied,
"hashes_filled": filled,
"sizes_corrected": resized,
"sizes_updated": resized,
"sizes_removed": unsized,
"hashes_cleared": cleared,
"requirements_corrected": requirements,
"required_aligned": requirements,
"withheld": list(withheld),
"found_by_content": report.labels,
"unattributed": report.unattributed,
+2 -1
View File
@@ -172,7 +172,8 @@ def fetch_logos() -> None:
def fetch_icons() -> None:
from generate_site import _group_by_manufacturer, load_database
from common import load_database
from sitecollection import _group_by_manufacturer
from siterender import SYSTEM_ICON_BASE, _icon_name
db = load_database(str(ROOT / "database.json"))
+57 -11
View File
@@ -13,6 +13,7 @@ from __future__ import annotations
import argparse
import json
import os
import re
import sys
from collections.abc import Callable
from datetime import datetime, timezone
@@ -40,6 +41,8 @@ from nativemode import check_label
import release_record
from siteguides import install_command
from validate_pr import LARGE_FILE_ISSUE_URL
from largefiles import load_registered_paths
import pr_checks
from verify import verify_platform
@@ -447,6 +450,29 @@ REPO = "Abdess/retrobios"
UPLOAD_URL = f"https://github.com/{REPO}/upload/main/bios"
def clone_line(db: dict) -> str:
"""How to get the clone the commands below run in, and its size.
Said bare, "clone the repo" started a multi-gigabyte transfer for a user
after a 20 MB --required-only pack. The size counts the collection files
the repository commits, a floor: release assets are fetched only when a
pack needs them.
"""
assets = set(load_registered_paths())
committed = sum(
entry.get("size", 0) for entry in db.get("files", {}).values()
if entry.get("path") not in assets
)
return (
"Packs are built from a clone, and the clone carries the collection:"
f" its checkout alone is over {format_size(committed)}."
f" `git clone --depth 1 https://github.com/{REPO}.git` leaves the"
" history out. Files kept as release assets are downloaded when a"
" pack needs them. Then generate packs for any platform, emulator,"
" or system:"
)
def _existing_contributor_block(readme_path: str = "README.md") -> list[str]:
"""The contributors already published, read back from the file.
@@ -767,7 +793,7 @@ def generate_readme(db: dict, platforms_dir: str, db_path: str = "database.json"
"",
"## Build your own pack",
"",
"Clone the repo and generate packs for any platform, emulator, or system:",
clone_line(db),
"",
"```bash",
"# The pack of a platform, as released",
@@ -891,6 +917,19 @@ def generate_readme(db: dict, platforms_dir: str, db_path: str = "database.json"
return "\n".join(lines) + "\n"
def ci_install_lines() -> tuple[str, str]:
"""The pip lines the test job of validate.yml runs, verbatim.
The guide listed the packages without their versions, so a local run
could pass on releases CI never installs. Read from the workflow, the
two cannot disagree. The second line is the Python 3.8 one.
"""
text = (_REPO / ".github" / "workflows" / "validate.yml").read_text(encoding="utf-8")
main = re.search(r'^\s*run: (pip install pyyaml "jsonschema.*markdown.*)$', text, re.M)
floor = re.search(r"^\s*run: (python3\.8 -m pip install .*)$", text, re.M)
return main.group(1), floor.group(1)
def contributing_text(
link: Callable[[str], str], title: str, site_words: str
) -> str:
@@ -900,6 +939,9 @@ def contributing_text(
site. *link* renders a site page (absolute URL for the README, relative
.md for the site); *site_words* names the site from where the text is read.
"""
pip_line, floor_line = ci_install_lines()
width = max(len(step.command) for step in pr_checks.STEPS)
steps = "\n".join(f"{step.command:<{width}} # {step.why}" for step in pr_checks.STEPS)
return f"""{title}
## Add a BIOS file
@@ -974,18 +1016,22 @@ validation above is its check. For code, profiles or platform files, these are
the checks CI runs on a pull request (Python 3.12 or newer):
```bash
pip install pyyaml "jsonschema[format-nongpl]" markdown pyte hypothesis vermin setuptools
python scripts/validate_pr.py --changed # the BIOS files the branch adds
python scripts/validate_schemas.py --source-only # profiles and platform files
python scripts/profile_sync.py --added --strict # each profile the branch adds
python scripts/refresh_data_dirs.py # data directories the pack-building tests read
python scripts/generate_site.py # the pages the site tests read
npm ci --prefix website && npm run build --prefix website # the built site, Node 24
python -m unittest discover -s tests -t .
python scripts/validate_site.py --site-dir website/dist # links and page budgets
vermin --no-tips -t=3.8- --violations install.py # install.py stays on Python 3.8
{pip_line}
python scripts/pr_checks.py
```
`pr_checks.py` runs these, in CI's order, carries on past a failure as CI
does, and names at the end each check that failed or could not run:
```bash
{steps}
```
The versions are the ones CI pins. Three tools come from the system, not from
that line, and the tests that need them skip without them: `shellcheck` for the
handheld launchers, `bwrap` (bubblewrap) for the probe sandbox, and a Python 3.8
with pyte (`{floor_line}`) for the full screen. CI has all three.
`profile_sync.py` compares the branch with this repository's `main`, and
fetches it in a fork's clone. A profile you only edit is not held to
`--strict`: its older refs are reviewed by hand.
+12 -172
View File
@@ -38,6 +38,10 @@ from pathlib import Path
from typing import TypeVar
sys.path.insert(0, os.path.dirname(__file__))
from sitecollection import ( # noqa: E402
_copy_places, _group_by_manufacturer, _profiles_in,
_profiles_on_disk, _slug, collection_paths,
)
from common import (
BUCKET_FOLDERS,
names_manufacturer,
@@ -71,10 +75,6 @@ from cross_reference import FileConsumers, entry_target, held_copy
from nativemode import check_label, digest_algorithm, reads_file_contents
yaml = require_yaml()
from generate_readme import (
compute_coverage,
@@ -139,8 +139,6 @@ GENERATED_DIRS = ["platforms", "systems", "emulators", "wiki", "api", "downloads
PUBLIC_DIRS = ("api", "downloads")
def _content_check_ceiling(profiles: dict) -> str:
"""How far content checking can reach across every profiled entry."""
hashed = sized = neither = 0
@@ -170,9 +168,6 @@ def _content_check_ceiling(profiles: dict) -> str:
)
CLS_LABELS = {
"official_port": "Official ports",
"community_fork": "Community forks",
@@ -202,33 +197,6 @@ CLS_LABEL_ONE = {
}
def _copy_places(db: dict) -> dict[str, set[tuple[str, str, str]]]:
"""sha1 -> every (manufacturer, console, file name) holding a copy.
The database keeps one path per content, deduplication having left one
copy. by_path_suffix still records the other folders a copy is filed in,
under the name that folder gives it.
"""
files = db.get("files", {})
bios_dir = Path(str(db.get("bios_dir") or "bios"))
places: dict[str, set[tuple[str, str, str]]] = {}
for sha1, entry in files.items():
parts = str(entry.get("path", "")).split("/")
if len(parts) >= 3 and parts[0] == "bios":
places.setdefault(sha1, set()).add((parts[1], parts[2], parts[-1]))
for tail, sha1s in db.get("indexes", {}).get("by_path_suffix", {}).items():
parts = tail.split("/")
if len(parts) < 3 or parts[1].startswith(".") or len(sha1s) != 1:
continue
if sha1s[0] in files and (bios_dir / tail).is_file():
places.setdefault(sha1s[0], set()).add((parts[0], parts[1], parts[-1]))
return places
def _slug(manufacturer: str) -> str:
return manufacturer.lower().replace(" ", "-")
def _copies_by_path(db: dict) -> dict[str, set[tuple[str, str, str]]]:
"""The copy places of each content, keyed by the path the database keeps."""
files = db.get("files", {})
@@ -414,11 +382,6 @@ def _system_pages_from_data(
return pages
def _timestamp() -> str:
"""The build time for data files (stats, API records), ISO 8601."""
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
@@ -437,8 +400,6 @@ def _date_line() -> str:
return f'<div class="rb-timestamp">{data_as_of()}.</div>'
# Home page
@@ -2486,10 +2447,16 @@ def _browser_title(relative: Path, title: str) -> str:
return title
_LINK_TEXT = r"(?:`[^`]*`|\[[^\]\[]*\]|[^\]\[])"
def _plain_markdown(text: str) -> str:
text = re.sub(r"<[^>]+>", " ", text)
text = re.sub(r"!\[([^]]*)\]\([^)]*\)", r"\1", text)
text = re.sub(r"\[([^]]+)\]\([^)]*\)", r"\1", text)
# Link text may hold brackets: a code span or a No-Intro name such as
# `[BIOS] LaserActive PAC-N1 (Japan) (v1.02).bin` left the whole link,
# anchor included, in the Pioneer page's meta description.
text = re.sub(rf"!\[({_LINK_TEXT}*)\]\([^)]*\)", r"\1", text)
text = re.sub(rf"\[({_LINK_TEXT}+)\]\([^)]*\)", r"\1", text)
# Code spans keep their text whole: validate_schemas.py read
# validateschemas.py once every underscore was taken for emphasis.
pieces = re.split(r"(`[^`]*`)", text)
@@ -5266,122 +5233,6 @@ def write_lookup_shards(
return len(shards)
def collection_paths(db: dict) -> dict[str, set[str]]:
"""Every path under bios/ that holds each file, by sha1.
The database keeps one path per content. The others are the full tails
by_path_suffix records that name a file on disk: RPG Maker, engine
trees and the copies filed under several consoles keep 2,502 of them.
"""
files = db.get("files", {})
bios_dir = Path(str(db.get("bios_dir") or "bios"))
paths: dict[str, set[str]] = {}
for sha1, entry in files.items():
path = str(entry.get("path", ""))
if path.startswith("bios/") and path.count("/") >= 2:
paths.setdefault(sha1, set()).add(path)
for tail, sha1s in db.get("indexes", {}).get("by_path_suffix", {}).items():
if tail.count("/") < 2 or len(sha1s) != 1 or sha1s[0] not in files:
continue
if (bios_dir / tail).is_file():
paths.setdefault(sha1s[0], set()).add(f"bios/{tail}")
return paths
def _group_by_manufacturer(db: dict) -> dict[str, dict[str, list]]:
"""Group files by manufacturer -> console -> files.
The database keeps one path per content, and a copy filed elsewhere was
on no page of its own: the Channel F BIOS sat under Arcade > FBNeo only,
and Fairchild had no system page. Every folder holding a copy lists it,
read from the full paths by_path_suffix records. The game engines'
files are grouped by engine (_engine_sections).
"""
files = db.get("files", {})
manufacturers: dict[str, dict[str, list]] = {}
for sha1, where in _copy_places(db).items():
for manufacturer, console in sorted({(m, c) for m, c, _ in where}):
manufacturers.setdefault(manufacturer, {}).setdefault(console, []).append(files[sha1])
return _engine_sections(manufacturers)
def _engine_sections(manufacturers: dict[str, dict[str, list]]) -> dict[str, dict[str, list]]:
"""A game engine's files in one section headed with the engine's name.
The Systems index listed ".variants", "scummvm.zip" and "wildmidi.cfg"
as games, and ScummVM's files sat on three pages. A folder of variants
joins the section of its primary, a file kept at a top's root joins
the section of the one engine that reads it (else of the engine the
top is named after), the folder under Other named after a game-engine
top joins that top, and a folder named by a profile key is headed with
the engine's name (SECTION_LABELS), its address unchanged.
"""
from siterender import _PROGRAM_TOPS, SECTION_LABELS, record_card_move # noqa: PLC0415
profiles = {key: profile for key, profile in _profiles_on_disk().items()
if profile.get("type") not in ("alias", "test")}
readers: dict[str, set[str]] = {}
for key, profile in profiles.items():
for entry in profile.get("files") or []:
if isinstance(entry, dict):
for name in [entry.get("name"), *(entry.get("aliases") or [])]:
if name:
readers.setdefault(str(name).rsplit("/", 1)[-1], set()).add(key)
def engine(key: str) -> str:
return str(profiles[key].get("emulator") or key)
def profile_of(name: str) -> str | None:
return next((k for k in sorted(profiles)
if name.casefold() in (k.casefold(), engine(k).casefold())), None)
out: dict[str, dict[str, list]] = {}
for mfr, consoles in manufacturers.items():
if mfr not in GAME_DATA_TOPS:
out[mfr] = dict(consoles)
continue
own = profile_of(mfr)
regrouped: dict[str, list] = {}
for console, files in sorted(consoles.items(), key=lambda item: item[0].startswith(".")):
for f in files:
parts = str(f.get("path", "")).split("/")
target = console
if console.startswith(".") or (len(parts) == 3 and console == parts[-1]):
# A variant answers its primary's name: scummvm.zip.a93f1c4b.
base = parts[-1].rsplit(".", 1)[0] if console.startswith(".") else parts[-1]
keys = sorted(readers.get(base, ()))
key = keys[0] if len(keys) == 1 else own
found = next((c for c, held in regrouped.items()
if any(h.get("name") == base for h in held)), None)
target = found or (next((c for c in consoles if c == key), None)
or (engine(key) if key else base))
if target != console:
record_card_move(mfr, console, str(f.get("sha1", "")), mfr, target)
regrouped.setdefault(target, []).append(f)
out[mfr] = regrouped
# The folder under Other an engine top is named after: ScummVM's
# engine data under Other/ScummVM, its themes under ScummVM/.
others = out.get("Other", {})
for folder in sorted(others):
top = next((t for t in out if t in GAME_DATA_TOPS and t.casefold() == folder.casefold()),
None)
if top is None or top in _PROGRAM_TOPS:
continue
sections = out[top]
target = next((c for c in sections if c.casefold() == folder.casefold()), folder)
held = {str(f.get("sha1", "")) for f in sections.get(target, [])}
for f in others[folder]:
record_card_move("Other", folder, str(f.get("sha1", "")), top, target)
sections[target] = [*sections.get(target, []),
*(f for f in others.pop(folder) if str(f.get("sha1", "")) not in held)]
for mfr in GAME_DATA_TOPS:
for console in out.get(mfr, {}):
key = profiles.get(console) and console
if key and engine(key) != console:
SECTION_LABELS[(mfr, console)] = engine(key)
return {mfr: consoles for mfr, consoles in out.items() if consoles}
def _console_display(manufacturer: str, console: str) -> str:
"""A console section's name as the reader looks for it: the heading
system_name gives its id, or the folder alone under a program's top
@@ -6696,15 +6547,6 @@ def _reading_pairs(sha1: str, consumers: FileConsumers) -> list[tuple[str, dict]
return pairs
def _profiles_on_disk() -> dict[str, dict]:
return _profiles_in(str(siteinputs.current().emulators), skip_aliases=False)
@functools.lru_cache(maxsize=None)
def _profiles_in(emulators_dir: str, skip_aliases: bool) -> dict[str, dict]:
return load_emulator_profiles(emulators_dir, skip_aliases=skip_aliases)
def _entry_systems(emulator: str, entry: dict) -> list[str]:
"""The systems an entry is read for: its own `system:` or `systems:`
(a MAME device set several systems load), else its profile's only one."""
@@ -11356,8 +11198,6 @@ def generate_gap_analysis(
return "\n".join(lines) + "\n"
def _core_scope_note(config: dict, profiles: dict) -> str:
"""" (all libretro)" when the platform resolves every libretro core.
+1 -1
View File
@@ -70,7 +70,7 @@ def system_cards() -> dict[str, tuple[str, str]]:
manufacturer and listing its consoles. Read from the same grouping the
system pages are written from."""
from common import GAME_DATA_TOPS, load_database
from generate_site import _group_by_manufacturer, _slug
from sitecollection import _group_by_manufacturer, _slug
def lead(manufacturer: str) -> str:
# Other holds one folder per emulator, a game publisher's page its
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env python3
"""Run the checks validate.yml runs on a pull request, in its order.
Usage:
python scripts/pr_checks.py
python scripts/pr_checks.py --list
Each step runs even when an earlier one failed, as CI runs them, and the
summary names every step that failed or could not run. A step whose tool is
absent (npm, python3.8, vermin) is reported as not run, with what provides it.
"""
from __future__ import annotations
import argparse
import os
import shutil
import subprocess
import sys
from dataclasses import dataclass, field
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
@dataclass(frozen=True)
class Step:
command: str
why: str
needs: str = ""
provider: str = ""
env: dict[str, str] = field(default_factory=dict)
# The order of validate.yml: data fetched, site generated and built, then the
# suite, its 3.8 run and the built-site check.
STEPS: tuple[Step, ...] = (
Step("python scripts/validate_pr.py --changed", "the BIOS files the branch adds"),
Step("python scripts/validate_schemas.py --source-only", "profiles and platform files"),
Step("python scripts/profile_sync.py --added --strict", "each profile the branch adds"),
Step("vermin --no-tips -t=3.8- --violations install.py", "install.py stays on Python 3.8",
needs="vermin", provider="the pip line of CONTRIBUTING.md"),
Step("python scripts/export_native.py --all --fetch --output-dir tmp/native",
"files the exporter tests compare with"),
Step("python scripts/refresh_data_dirs.py", "data directories the pack-building tests read"),
Step("python scripts/generate_site.py", "the pages the site tests read"),
Step("npm ci --prefix website", "the site's packages, Node 24",
needs="npm", provider="Node 24"),
Step("npm run build --prefix website", "the built site",
needs="npm", provider="Node 24"),
Step("python -m unittest discover -s tests -t .", "the suite",
env={"RETROBIOS_OFFLINE": "1"}),
Step("python3.8 -m unittest tests.test_install_tui", "the full screen on the installer's floor",
needs="python3.8", provider="a Python 3.8 with pyte",
env={"RETROBIOS_OFFLINE": "1"}),
Step("python scripts/validate_site.py --site-dir website/dist", "links and page budgets"),
)
def argv(step: Step) -> list[str]:
"""The command, with `python` run by the interpreter running this script."""
words = step.command.split()
return [sys.executable, *words[1:]] if words[0] == "python" else words
def run(steps: tuple[Step, ...] = STEPS) -> int:
failed: list[str] = []
not_run: list[str] = []
for step in steps:
print(f"\n== {step.command}", flush=True)
if step.needs and shutil.which(step.needs) is None:
print(f"not run: {step.needs} is not installed ({step.provider})")
not_run.append(f"{step.command} ({step.needs}: {step.provider})")
continue
code = subprocess.run(argv(step), cwd=ROOT, env={**os.environ, **step.env}).returncode
if code:
failed.append(f"{step.command} (exit {code})")
print()
for line in failed:
print(f"FAILED {line}")
for line in not_run:
print(f"NOT RUN {line}")
print(f"{len(steps) - len(failed) - len(not_run)}/{len(steps)} checks passed")
return 1 if failed else 0
def main() -> int:
parser = argparse.ArgumentParser(allow_abbrev=False, description=__doc__.splitlines()[0])
parser.add_argument("--list", action="store_true", help="print the steps and exit")
args = parser.parse_args()
if args.list:
for step in STEPS:
print(step.command)
return 0
return run()
if __name__ == "__main__":
from common import run_cli
run_cli(main)
+169
View File
@@ -0,0 +1,169 @@
"""The collection as the site files it: every folder holding each file.
The database keeps one path per content. The folders a copy is also filed
in, the system sections and the game-engine sections are read here, by the
page generator and by the tools that draw or fetch per-system images. The
bios folder comes from siteinputs, never the working directory.
"""
from __future__ import annotations
import functools
import os
import sys
sys.path.insert(0, os.path.dirname(__file__))
import siteinputs # noqa: E402
from common import GAME_DATA_TOPS, load_emulator_profiles # noqa: E402
def _copy_places(db: dict) -> dict[str, set[tuple[str, str, str]]]:
"""sha1 -> every (manufacturer, console, file name) holding a copy.
The database keeps one path per content, deduplication having left one
copy. by_path_suffix still records the other folders a copy is filed in,
under the name that folder gives it.
"""
files = db.get("files", {})
bios_dir = siteinputs.bios_dir(db)
places: dict[str, set[tuple[str, str, str]]] = {}
for sha1, entry in files.items():
parts = str(entry.get("path", "")).split("/")
if len(parts) >= 3 and parts[0] == "bios":
places.setdefault(sha1, set()).add((parts[1], parts[2], parts[-1]))
for tail, sha1s in db.get("indexes", {}).get("by_path_suffix", {}).items():
parts = tail.split("/")
if len(parts) < 3 or parts[1].startswith(".") or len(sha1s) != 1:
continue
if sha1s[0] in files and (bios_dir / tail).is_file():
places.setdefault(sha1s[0], set()).add((parts[0], parts[1], parts[-1]))
return places
def _slug(manufacturer: str) -> str:
return manufacturer.lower().replace(" ", "-")
def collection_paths(db: dict) -> dict[str, set[str]]:
"""Every path under bios/ that holds each file, by sha1.
The database keeps one path per content. The others are the full tails
by_path_suffix records that name a file on disk: RPG Maker, engine
trees and the copies filed under several consoles keep 2,502 of them.
"""
files = db.get("files", {})
bios_dir = siteinputs.bios_dir(db)
paths: dict[str, set[str]] = {}
for sha1, entry in files.items():
path = str(entry.get("path", ""))
if path.startswith("bios/") and path.count("/") >= 2:
paths.setdefault(sha1, set()).add(path)
for tail, sha1s in db.get("indexes", {}).get("by_path_suffix", {}).items():
if tail.count("/") < 2 or len(sha1s) != 1 or sha1s[0] not in files:
continue
if (bios_dir / tail).is_file():
paths.setdefault(sha1s[0], set()).add(f"bios/{tail}")
return paths
def _group_by_manufacturer(db: dict) -> dict[str, dict[str, list]]:
"""Group files by manufacturer -> console -> files.
The database keeps one path per content, and a copy filed elsewhere was
on no page of its own: the Channel F BIOS sat under Arcade > FBNeo only,
and Fairchild had no system page. Every folder holding a copy lists it,
read from the full paths by_path_suffix records. The game engines'
files are grouped by engine (_engine_sections).
"""
files = db.get("files", {})
manufacturers: dict[str, dict[str, list]] = {}
for sha1, where in _copy_places(db).items():
for manufacturer, console in sorted({(m, c) for m, c, _ in where}):
manufacturers.setdefault(manufacturer, {}).setdefault(console, []).append(files[sha1])
return _engine_sections(manufacturers)
def _engine_sections(manufacturers: dict[str, dict[str, list]]) -> dict[str, dict[str, list]]:
"""A game engine's files in one section headed with the engine's name.
The Systems index listed ".variants", "scummvm.zip" and "wildmidi.cfg"
as games, and ScummVM's files sat on three pages. A folder of variants
joins the section of its primary, a file kept at a top's root joins
the section of the one engine that reads it (else of the engine the
top is named after), the folder under Other named after a game-engine
top joins that top, and a folder named by a profile key is headed with
the engine's name (SECTION_LABELS), its address unchanged.
"""
from siterender import _PROGRAM_TOPS, SECTION_LABELS, record_card_move # noqa: PLC0415
profiles = {key: profile for key, profile in _profiles_on_disk().items()
if profile.get("type") not in ("alias", "test")}
readers: dict[str, set[str]] = {}
for key, profile in profiles.items():
for entry in profile.get("files") or []:
if isinstance(entry, dict):
for name in [entry.get("name"), *(entry.get("aliases") or [])]:
if name:
readers.setdefault(str(name).rsplit("/", 1)[-1], set()).add(key)
def engine(key: str) -> str:
return str(profiles[key].get("emulator") or key)
def profile_of(name: str) -> str | None:
return next((k for k in sorted(profiles)
if name.casefold() in (k.casefold(), engine(k).casefold())), None)
out: dict[str, dict[str, list]] = {}
for mfr, consoles in manufacturers.items():
if mfr not in GAME_DATA_TOPS:
out[mfr] = dict(consoles)
continue
own = profile_of(mfr)
regrouped: dict[str, list] = {}
for console, files in sorted(consoles.items(), key=lambda item: item[0].startswith(".")):
for f in files:
parts = str(f.get("path", "")).split("/")
target = console
if console.startswith(".") or (len(parts) == 3 and console == parts[-1]):
# A variant answers its primary's name: scummvm.zip.a93f1c4b.
base = parts[-1].rsplit(".", 1)[0] if console.startswith(".") else parts[-1]
keys = sorted(readers.get(base, ()))
key = keys[0] if len(keys) == 1 else own
found = next((c for c, held in regrouped.items()
if any(h.get("name") == base for h in held)), None)
target = found or (next((c for c in consoles if c == key), None)
or (engine(key) if key else base))
if target != console:
record_card_move(mfr, console, str(f.get("sha1", "")), mfr, target)
regrouped.setdefault(target, []).append(f)
out[mfr] = regrouped
# The folder under Other an engine top is named after: ScummVM's
# engine data under Other/ScummVM, its themes under ScummVM/.
others = out.get("Other", {})
for folder in sorted(others):
top = next((t for t in out if t in GAME_DATA_TOPS and t.casefold() == folder.casefold()),
None)
if top is None or top in _PROGRAM_TOPS:
continue
sections = out[top]
target = next((c for c in sections if c.casefold() == folder.casefold()), folder)
held = {str(f.get("sha1", "")) for f in sections.get(target, [])}
for f in others[folder]:
record_card_move("Other", folder, str(f.get("sha1", "")), top, target)
sections[target] = [*sections.get(target, []),
*(f for f in others.pop(folder) if str(f.get("sha1", "")) not in held)]
for mfr in GAME_DATA_TOPS:
for console in out.get(mfr, {}):
key = profiles.get(console) and console
if key and engine(key) != console:
SECTION_LABELS[(mfr, console)] = engine(key)
return {mfr: consoles for mfr, consoles in out.items() if consoles}
def _profiles_on_disk() -> dict[str, dict]:
return _profiles_in(str(siteinputs.current().emulators), skip_aliases=False)
@functools.lru_cache(maxsize=None)
def _profiles_in(emulators_dir: str, skip_aliases: bool) -> dict[str, dict]:
return load_emulator_profiles(emulators_dir, skip_aliases=skip_aliases)
+12 -12
View File
@@ -22,9 +22,9 @@ API_EXPORTS = "api/v1/exports"
# Report key, then how a line of it reads on the page.
CHANGE_KINDS = (
("hashes_corrected", "Hashes updated to the emulator source"),
("hashes_updated", "Hashes updated to the emulator source"),
("hashes_filled", "Hashes added where the original lists none"),
("sizes_corrected", "Sizes updated to the emulator source"),
("sizes_updated", "Sizes updated to the emulator source"),
("sizes_removed", "Sizes no longer stated"),
("hashes_cleared", "Hashes no longer listed"),
("withheld", "Changes not applied"),
@@ -33,9 +33,9 @@ CHANGE_KINDS = (
# Report key, then the kind each line of `changes` carries.
CHANGE_RECORDS = {
"hashes_corrected": "hash_corrected",
"hashes_updated": "hash_updated",
"hashes_filled": "hash_filled",
"sizes_corrected": "size_corrected",
"sizes_updated": "size_updated",
"sizes_removed": "size_removed",
"hashes_cleared": "hash_cleared",
}
@@ -52,11 +52,11 @@ def describe_change(change: dict) -> str:
"""One change in words, with the value before and after it."""
label = f"`{change['system']}/{change['name']}` {change['field']}"
kind, old, new = change["kind"], change["old"], change["new"]
if kind in ("hash_corrected", "size_corrected"):
if kind in ("hash_updated", "size_updated"):
return f"{label} {_value(old)} becomes {_value(new)}"
if kind == "hash_filled":
return f"{label} {_value(new)} added"
if kind == "requirement_corrected":
if kind == "required_aligned":
return f"`{change['system']}/{change['name']}` becomes {'required' if new else 'optional'}"
return f"{label} {_value(old)} no longer listed"
@@ -76,7 +76,7 @@ def requirement_lines(report: dict) -> list[str]:
"""Each required flag aligned with the code, and what it is now."""
items = [
describe_change(c) for c in report.get("changes") or []
if c["kind"] == "requirement_corrected"
if c["kind"] == "required_aligned"
]
if not items:
return []
@@ -108,11 +108,11 @@ def change_text(report: dict) -> str:
parts = [f"{_plural(counts['kept'], 'entry', 'entries')} kept"]
extra = [
(counts["added"], "file", "files", "added"),
(len(report.get("hashes_corrected") or []), "hash", "hashes", "updated"),
(report.get("requirements_corrected") or 0, "required flag", "required flags",
(len(report.get("hashes_updated") or []), "hash", "hashes", "updated"),
(report.get("required_aligned") or 0, "required flag", "required flags",
"aligned with the code"),
(len(report.get("hashes_filled") or []), "hash", "hashes", "added"),
(len(report.get("sizes_corrected") or []), "size", "sizes", "updated"),
(len(report.get("sizes_updated") or []), "size", "sizes", "updated"),
]
parts += [f"{_plural(n, one, many)} {verb}" for n, one, many, verb in extra if n]
return ", ".join(parts)
@@ -216,7 +216,7 @@ def _display(platform: str, names: dict[str, str]) -> str:
return names.get(platform) or platform
def _corrected_link(report: dict) -> str:
def _updated_link(report: dict) -> str:
platform, files = report["platform"], report["files"]
if len(files) == 1:
path = f"{EXPORTS_DIR}/{platform}/{files[0]}"
@@ -286,7 +286,7 @@ def generate_exports_page(reports: list[dict], names: dict[str, str]) -> str:
platform = report["platform"]
lines.append(
f"| [{_display(platform, names)}](#{platform}) | "
f"{_original_link(report)} | {_corrected_link(report)} | "
f"{_original_link(report)} | {_updated_link(report)} | "
f"{change_text(report)} |"
)
lines.append("")
+11
View File
@@ -30,3 +30,14 @@ def current() -> SiteInputs:
def use(inputs: SiteInputs) -> None:
_current[0] = inputs
def bios_dir(db: dict) -> Path:
"""The database's bios folder, as the inputs place it.
database.json names it relative to its own folder ("bios"). Read from the
working directory instead, a tool started elsewhere found no copy on disk
and dropped five system sections and 2,500 collection paths in silence.
"""
named = Path(str(db.get("bios_dir") or "bios"))
return named if named.is_absolute() else current().db.parent / named
+2 -2
View File
@@ -733,8 +733,8 @@ def data_page_section(path: Path) -> list[str]:
"",
"[`measurements.sqlite`](downloads/measurements.sqlite) holds every core probe "
f"record kept in [`{mr.RECORDS_DIR}/`]({REPO}/tree/main/{mr.RECORDS_DIR}), and "
"`downloads/measurements/<emulator>.sqlite` the records of one emulator, linked "
f"from its page. The [Measurements]({EXPLORER_PAGE}) page reads "
"`downloads/measurements/<emulator>.sqlite` the records of one emulator. The "
f"[Measurements]({EXPLORER_PAGE}) page reads "
"[`data/measurements/index.json`](data/measurements/index.json). A record line is "
"valid against [`core-probe-record.schema.json`](api/v1/schemas/core-probe-record.schema.json).",
"",
+1 -1
View File
@@ -780,7 +780,7 @@ def _ensure_console_sections() -> None:
import json
from common import load_emulator_profiles
from generate_site import _group_by_manufacturer
from sitecollection import _group_by_manufacturer
inputs = siteinputs.current()
with open(inputs.db, encoding="utf-8") as handle:
+18 -5
View File
@@ -16,9 +16,10 @@ from dataclasses import dataclass
from pathlib import Path
import upstream
from siterender import _by_mode
from siterender import _by_mode, _escape_tags
from sourceref import (
_HEX_ADDRESS_RE, _prose_runs, is_binary_citation, ref_role, source_ref_values,
_HEX_ADDRESS_RE, _prose_runs, is_binary_citation, ref_annotation, ref_role,
source_ref_values,
split_source_ref,
)
@@ -512,7 +513,7 @@ def source_ref_items(profile: dict, value: str | dict | None) -> list[tuple[str,
for part in split_source_ref(refs):
if is_binary_citation(part.path):
text, binary = _binary_citation(part.path, binary)
rendered.append((text, ""))
rendered.append((text + _annotation_markdown(ref_annotation(part.raw)), ""))
continue
binary = ""
role, bare = ref_role(part.path)
@@ -544,19 +545,31 @@ def source_ref_items(profile: dict, value: str | dict | None) -> list[tuple[str,
role = (role_label(profile, current_role, part.path)
if current_role and _two_repositories(profile) else "")
note = _annotation_markdown(ref_annotation(part.raw))
if selected is None:
rendered.append((f"`{display}`", role))
rendered.append((f"`{display}`{note}", role))
else:
repo, pin = selected
url = _source_permalink(
repo, pin, real_path, part.start, part.end
)
rendered.append((f"[`{display}`]({url})", role))
rendered.append((f"[`{display}`]({url}){note}", role))
groups.append((label, rendered or [(f"`{refs}`", "")]))
return groups
def _annotation_markdown(text: str) -> str:
"""A part's annotation after its citation, as written: `(lsf default)`.
Profile prose, so placeholders keep their brackets and nothing in it
opens emphasis or a table cell.
"""
if not text:
return ""
return " " + _escape_tags(re.sub(r"([*|\\])", r"\\\1", text))
def _binary_citation(path: str, previous: str) -> tuple[str, str]:
"""A part citing code inside a shipped binary, and the binary it names.
+19
View File
@@ -123,6 +123,25 @@ def split_source_ref(ref: str) -> list[RefPart]:
return parts
_LOCATION_RE = re.compile(r":\d+(?:-\d+)?|^\s*\d+(?:-\d+)?(?=\s|$)")
def ref_annotation(raw: str) -> str:
"""The words a part writes beside its location, for a reader.
split_source_ref drops them because a comparison has no use for them,
but `src/loader2.cpp:284 (lsf default), src/netcfg.cpp:204 (lsf
override)` says which line wins only through them, and 1,103 of the
profiles' annotations were on no page.
"""
paren = raw.find("(")
head = raw if paren < 0 else raw[:paren]
located = _LOCATION_RE.search(head)
if located:
return raw[located.end():].strip()
return raw[paren:].strip() if paren >= 0 else ""
def ref_prose_text(ref: str) -> str:
"""The text of a source_ref its parts do not read, positions kept.
+65
View File
@@ -7574,5 +7574,70 @@ class TupleAnnotationsMatchTheirReturns(unittest.TestCase):
self.assertEqual(mismatched, [])
class PrboomPlusCitesFullPaths(unittest.TestCase):
"""prboom-plus cited gl_hires.c, e6y.c, d_client.c and g_game.c bare:
none resolves against the repository, so the page showed them as plain
code with no link, and "behind GL_DOOM and PRBOOM_DEBUG" was read as a
seventeenth citation. Each name has one path in the tree at the pin."""
def test_every_cited_file_has_its_directory(self):
import yaml
from scripts.sourceref import source_ref_values, split_source_ref
profile = yaml.safe_load((ROOT / "emulators" / "prboom-plus.yml").read_text(encoding="utf-8"))
refs: list[str] = []
def walk(node):
if isinstance(node, dict):
for key, value in node.items():
if key == "source_ref":
refs.extend(r for _, r in source_ref_values(value))
else:
walk(value)
elif isinstance(node, list):
for item in node:
walk(item)
walk(profile)
parts = [p for ref in refs for p in split_source_ref(ref)]
self.assertGreater(len(parts), 40)
for part in parts:
with self.subTest(path=part.path):
self.assertIsNotNone(part.start)
self.assertTrue(part.path.startswith("prboom2/"), part.path)
class AutoFetchIssueBodyReadsAsWritten(unittest.TestCase):
"""The --create-issues body was a template: Title Case headings, "The
following...", an exclamation mark, numbered orders to the reader, a
spaced dash, and hashes cut to 12 characters a contributor cannot
match a file against."""
ENTRY = {"name": "scph5501.bin", "system": "sony-playstation",
"sha1": "b056ee5a4d65937e1a3a17e1e78f3258ea49c38e",
"md5": "490f666e1afb15b7362b406ed1cea246"}
def test_full_hashes_and_no_template_phrasing(self):
from scripts import auto_fetch
body = auto_fetch.generate_issue_body([self.ENTRY], "RetroArch")
self.assertIn(f"`{self.ENTRY['sha1']}`", body)
self.assertIn(f"`{self.ENTRY['md5']}`", body)
self.assertNotIn("...", body)
self.assertNotIn("!", body)
self.assertNotRegex(body, r"(?i)the following|please| - |\bfork\b")
self.assertNotRegex(body, r"(?m)^\d+\. ")
for heading in re.findall(r"(?m)^#+ (.*)$", body):
later = [w for w in heading.split()[1:] if w != "RetroArch"]
self.assertEqual(later, [w.lower() for w in later], heading)
def test_the_docstring_says_nothing_is_posted(self):
from scripts import auto_fetch
self.assertNotIn("Create GitHub Issue", auto_fetch.__doc__)
self.assertIn("posts nothing", auto_fetch.__doc__)
if __name__ == "__main__":
unittest.main()
+2 -2
View File
@@ -345,7 +345,7 @@ class SeveralAcceptedMd5BecomeAList(unittest.TestCase):
self.assertEqual(
produced, LISTING.replace(f'"md5": "{C}"', f'"md5": [ "{C}", "{D}" ]')
)
record = next(r for r in changes.records if r["kind"] == "hash_corrected")
record = next(r for r in changes.records if r["kind"] == "hash_updated")
self.assertEqual((record["old"], record["new"]), (C, [C, D]))
self.assertEqual(changes.lists, 1)
@@ -433,7 +433,7 @@ class SeveralAcceptedMd5BecomeAList(unittest.TestCase):
class SitePageShowsAList(unittest.TestCase):
def test_a_list_reads_as_any_of_its_values(self):
line = describe_change({
"kind": "hash_corrected", "system": "psx", "name": "scph5500.bin",
"kind": "hash_updated", "system": "psx", "name": "scph5500.bin",
"field": "md5", "old": C, "new": [C, D],
})
self.assertEqual(line, f"`psx/scph5500.bin` md5 `{C}` becomes `{C}` or `{D}`")
+110 -3
View File
@@ -37,7 +37,6 @@ class OneContributingText(unittest.TestCase):
)
class CoverageReadsTheGivenProfiles(unittest.TestCase):
"""generate_site --emulators-dir changed the emulator pages but not the
platform coverage, which read ./emulators whatever was given."""
@@ -67,7 +66,7 @@ class BeforeOpeningAPullRequest(unittest.TestCase):
text = (REPO_ROOT / "CONTRIBUTING.md").read_text(encoding="utf-8")
section = text.split("## Before opening a PR", 1)[1].split("\n## ", 1)[0]
block = _re.search(r"```bash\n(.*?)```", section, _re.S).group(1)
block = "".join(_re.findall(r"```bash\n(.*?)```", section, _re.S))
for command in ("validate_pr.py --changed", "validate_schemas.py --source-only",
"unittest discover"):
self.assertIn(command, block)
@@ -116,11 +115,41 @@ class BeforeOpeningAPullRequest(unittest.TestCase):
line naming the old theme installed packages nothing reads."""
text = generate_readme.generate_contributing()
section = text.split("## Before opening a PR", 1)[1].split("\n## ", 1)[0]
self.assertIn('pip install pyyaml "jsonschema[format-nongpl]" markdown', section)
self.assertIn('pip install pyyaml "jsonschema[format-nongpl]==', section)
self.assertNotIn("mkdocs", section)
self.assertTrue((REPO_ROOT / "website" / "package-lock.json").is_file())
class TheChecklistInstallsWhatCiInstalls(unittest.TestCase):
"""The pip line named the packages without versions, and nothing said
that shellcheck, bubblewrap and a Python 3.8 with pyte come from the
system: their tests skipped locally and failed first in CI."""
def test_the_pip_lines_are_the_workflow_lines(self):
workflow = (REPO_ROOT / ".github" / "workflows" / "validate.yml").read_text(encoding="utf-8")
text = generate_readme.generate_contributing()
section = text.split("## Before opening a PR", 1)[1].split("\n## ", 1)[0]
pinned = [line.split("run: ", 1)[1] for line in workflow.splitlines()
if re.match(r"\s*run: (python3\.8 -m )?pip install .*==", line)
and "pyte" in line]
self.assertEqual(len(pinned), 2)
for line in pinned:
with self.subTest(line=line):
self.assertIn(line, section)
def test_it_names_the_system_tools_ci_installs(self):
workflow = (REPO_ROOT / ".github" / "workflows" / "validate.yml").read_text(encoding="utf-8")
text = generate_readme.generate_contributing()
section = text.split("## Before opening a PR", 1)[1].split("\n## ", 1)[0]
for tool, step in (("shellcheck", "shellcheck --version"),
("bwrap", "bwrap --ro-bind"),
("python3.8 -m unittest tests.test_install_tui",
"python3.8 -m unittest tests.test_install_tui")):
with self.subTest(tool=tool):
self.assertIn(step, workflow)
self.assertIn(tool, section)
class OneFileThroughTheBrowser(unittest.TestCase):
"""Adding one BIOS asked for a fork, a 5.5 GB clone, five pip packages,
the data directories and the whole suite, and promised a PR comment that
@@ -174,5 +203,83 @@ class OneFileThroughTheBrowser(unittest.TestCase):
))
class TheGuideRebuildsWhenItsPinsMove(unittest.TestCase):
"""The checklist quotes validate.yml, so a pin bump there must redeploy
the guide and meet the README/CONTRIBUTING freshness check."""
def test_deploy_site_watches_the_validation_workflow(self):
import yaml
deploy = yaml.safe_load(
(REPO_ROOT / ".github" / "workflows" / "deploy-site.yml").read_text(encoding="utf-8"))
paths = deploy[True]["push"]["paths"]
self.assertIn(".github/workflows/validate.yml", paths)
class OneCommandRunsTheChecks(unittest.TestCase):
"""A contributor copied ten commands by hand before a pull request, and
skipping one (refresh_data_dirs) failed the pack tests with no hint."""
CHECK = re.compile(r"(?:xargs -0 |run: |^\s*)((?:python3?\.?8? -m unittest|python scripts/\w+\.py"
r"|vermin|npm (?:ci|run build)))")
def test_every_check_ci_runs_is_a_step(self):
import pr_checks
workflow = (REPO_ROOT / ".github" / "workflows" / "validate.yml").read_text(encoding="utf-8")
ran = {m.group(1) for line in workflow.splitlines()
if not line.strip().startswith("#") for m in [self.CHECK.search(line)] if m}
self.assertGreaterEqual(len(ran), 10)
steps = [step.command for step in pr_checks.STEPS]
for check in sorted(ran):
with self.subTest(check=check):
self.assertTrue(any(command.startswith(check) for command in steps))
def test_steps_keep_ci_order(self):
import pr_checks
workflow = (REPO_ROOT / ".github" / "workflows" / "validate.yml").read_text(encoding="utf-8")
order = [workflow.index(key) for key in (
"refresh_data_dirs.py", "run: python scripts/generate_site.py",
"npm run build --prefix website", "python -m unittest discover",
"validate_site.py")]
self.assertEqual(order, sorted(order))
commands = [step.command for step in pr_checks.STEPS]
positions = [next(i for i, c in enumerate(commands) if key in c) for key in (
"refresh_data_dirs.py", "generate_site.py", "npm run build",
"unittest discover", "validate_site.py")]
self.assertEqual(positions, sorted(positions))
def test_the_guide_gives_the_runner_and_what_it_runs(self):
import pr_checks
text = generate_readme.generate_contributing()
section = text.split("## Before opening a PR", 1)[1].split("\n## ", 1)[0]
self.assertIn("python scripts/pr_checks.py\n", section)
for step in pr_checks.STEPS:
with self.subTest(step=step.command):
self.assertIn(step.command, section)
def test_a_missing_tool_is_reported_and_a_failure_does_not_stop_the_run(self):
import contextlib
import io
import pr_checks
steps = (
pr_checks.Step("python -c raise(SystemExit(3))", "fails"),
pr_checks.Step("no-such-tool-here --x", "absent", needs="no-such-tool-here",
provider="nothing"),
pr_checks.Step("python -c pass", "passes"),
)
out = io.StringIO()
with contextlib.redirect_stdout(out):
code = pr_checks.run(steps)
self.assertEqual(code, 1)
self.assertIn("FAILED python -c raise(SystemExit(3)) (exit 3)", out.getvalue())
self.assertIn("NOT RUN no-such-tool-here --x", out.getvalue())
self.assertIn("1/3 checks passed", out.getvalue())
if __name__ == "__main__":
unittest.main()
+95 -2
View File
@@ -1985,7 +1985,6 @@ class NoTextClaimsAScheduledScrape(unittest.TestCase):
self.assertNotIn("list_platforms", workflow.read_text(encoding="utf-8"))
class TheCatalogRowsStateFacts(unittest.TestCase):
"""tools.md said "redump.org serves stale DATs" and "Dat-o-Matic blocks
automation", verdicts on two preservation projects, and gave Redump's
@@ -2003,6 +2002,100 @@ class TheCatalogRowsStateFacts(unittest.TestCase):
self.assertIn("`/bios/<CODE>`", rows[0])
class RegionFreeRuleNamesWhereItDoesNotHold(unittest.TestCase):
"""advanced-usage.md said psxonpsp660.bin is declared [world] and always
survives --region. PCSX ReARMed declares it [japan], and on the platforms
where it is the only PlayStation core the filter drops it."""
def test_every_platform_that_drops_a_named_file_is_named(self):
from common import (list_registered_platforms, load_emulator_profiles,
load_platform_config)
from region import WORLD, platform_region_index
section = _section(_page("advanced-usage.md"), "Region filtering")
bullet = next(p for p in section.split("\n- ") if "Region-free" in p)
files = re.findall(r"`([^`]+\.bin)`", bullet)
self.assertTrue(files)
profiles = load_emulator_profiles(str(ROOT / "emulators"))
for platform in list_registered_platforms(str(ROOT / "platforms")):
config = load_platform_config(platform, str(ROOT / "platforms"))
index = platform_region_index(config, profiles)
for name in files:
entry = index.get(name)
drops = bool(entry and entry["regions"] and WORLD not in entry["regions"]
and not entry["has_untagged"])
if drops:
with self.subTest(platform=platform, file=name):
self.assertRegex(bullet, rf"(?i)\b{re.escape(platform)}\b")
class BuildYourOwnPackSaysWhatTheCloneCosts(unittest.TestCase):
"""The README sent a user after a minimal pack to "Clone the repo", with no
command and no size, and the clone runs to several gigabytes."""
def test_readme_gives_the_clone_command_and_its_size(self):
import generate_readme
db = json.loads((ROOT / "database.json").read_text(encoding="utf-8"))
line = generate_readme.clone_line(db)
self.assertIn("git clone --depth 1", line)
self.assertRegex(line, r"over \d+(\.\d)? GB")
section = _section((ROOT / "README.md").read_text(encoding="utf-8"),
"Build your own pack")
self.assertIn("git clone --depth 1", section)
def test_advanced_usage_names_the_shallow_clone(self):
self.assertIn("--depth 1", _page("advanced-usage.md").split("## Custom packs")[0])
class LandingAChangeNamesTheCommitRules(unittest.TestCase):
"""The page left out the review-tag rule scripts/commit_rules.py
enforces, and never said where the rules live or how a clone runs them:
the hook is not versioned."""
def _landing(self) -> str:
return _section(_page("testing-guide.md"), "Landing a change")
def test_every_refusal_is_on_the_page(self):
import commit_rules
section = self._landing()
words = re.search(r"\\b\((.*?)\)", commit_rules.REFUSED_WORDS.pattern).group(1)
from tests.test_prose_style import NoMarketingVocabulary
for word in words.replace("|", " ").split():
if NoMarketingVocabulary.WORDS.search(word):
# Named by "the marketing vocabulary above": spelled out,
# the page would fail that test.
continue
with self.subTest(word=word):
self.assertIn(word, section.lower())
self.assertIn("which review asked for it", section)
self.assertIn("scripts/commit_rules.py", section)
def test_the_hook_command_on_the_page_refuses_a_review_tag(self):
import subprocess
import tempfile
block = re.search(r"```bash\n(.*?)```", self._landing(), re.S).group(1)
with tempfile.TemporaryDirectory(dir=ROOT / "tmp") as scratch:
clone = Path(scratch)
(clone / ".git" / "hooks").mkdir(parents=True)
(clone / "scripts").mkdir()
(clone / "scripts" / "commit_rules.py").write_bytes(
(ROOT / "scripts" / "commit_rules.py").read_bytes())
subprocess.run(["sh", "-c", block], cwd=clone, check=True)
message = clone / "MSG"
message.write_text("fix: address review 3 notes\n", encoding="utf-8")
refused = subprocess.run([str(clone / ".git" / "hooks" / "commit-msg"), "MSG"],
cwd=clone, capture_output=True, text=True)
message.write_text("fix: say what the change does\n", encoding="utf-8")
accepted = subprocess.run([str(clone / ".git" / "hooks" / "commit-msg"), "MSG"],
cwd=clone, capture_output=True, text=True)
self.assertEqual(refused.returncode, 1, refused.stderr)
self.assertIn("which review asked for it", refused.stderr)
self.assertEqual(accepted.returncode, 0, accepted.stderr)
if __name__ == "__main__":
unittest.main()
+26 -2
View File
@@ -436,8 +436,6 @@ class PagesOpenOnTheirContent(unittest.TestCase):
self.assertEqual(announcing, [])
class CoreCountsReadFromTheData(unittest.TestCase):
"""The cores page said "623 cores across 18 platforms": that was every
profile, and 91 of them no platform runs."""
@@ -611,5 +609,31 @@ class EveryOptionExists(unittest.TestCase):
if (bad := unknown_options(text))}
self.assertEqual(wrong, {})
class ContributorChecksFetchThePlatformsFiles(unittest.TestCase):
"""validate.yml fetches each platform's own file before the suite. The
checklist left it out, so the exporter fidelity cases skipped on a
contributor's clone and first ran, red, in CI."""
def test_the_pr_checklist_fetches_them_before_the_tests(self):
workflow = _read(".github/workflows/validate.yml")
self.assertLess(
workflow.index("python scripts/export_native.py --all --fetch"),
workflow.index("python -m unittest discover"),
)
for text in (
contributing_text(lambda page: page, "# C", "here"),
_read("CONTRIBUTING.md"),
):
block = _section(text, "## Before opening a PR")
self.assertIn("python scripts/export_native.py --all --fetch", block)
self.assertLess(
block.index("export_native.py"), block.index("unittest discover")
)
def test_the_testing_guide_names_the_command(self):
self.assertIn("export_native.py --all --fetch", _read("wiki/testing-guide.md"))
if __name__ == "__main__":
unittest.main()
+1 -1
View File
@@ -1083,7 +1083,7 @@ class AnExtendedListIsReported(unittest.TestCase):
self.assertEqual(fe.hashes("md5"), [A, B])
self.assertEqual(
[(r["kind"], r["field"], r["old"], r["new"]) for r in records],
[("hash_corrected", "md5", A, [A, B])],
[("hash_updated", "md5", A, [A, B])],
)
def test_an_unchanged_value_reports_nothing(self):
+28 -12
View File
@@ -29,8 +29,8 @@ def _report(platform: str, files: list[str], **changes: object) -> dict:
"originals": {f: f"https://example.org/{platform}/{f}" for f in files},
"files": files,
"summary": "3 kept, 1 file added, 7 hashes updated to the emulator source",
"hashes_corrected": [], "hashes_filled": [], "sizes_corrected": [],
"sizes_removed": [], "hashes_cleared": [], "requirements_corrected": 0,
"hashes_updated": [], "hashes_filled": [], "sizes_updated": [],
"sizes_removed": [], "hashes_cleared": [], "required_aligned": 0,
"withheld": [], "invalid": [],
}
report.update(changes)
@@ -58,8 +58,8 @@ class ExportReportIsWrittenAndRead(unittest.TestCase):
body = source[source.index("def export_platform("):source.index("def write_report(")]
call = body[body.index("write_report("):]
for key, value in (
("hashes_corrected", "applied"), ("hashes_filled", "filled"),
("sizes_corrected", "resized"), ("sizes_removed", "unsized"),
("hashes_updated", "applied"), ("hashes_filled", "filled"),
("sizes_updated", "resized"), ("sizes_removed", "unsized"),
("hashes_cleared", "cleared"), ("invalid", "issues"),
):
self.assertIn(f'"{key}": {value},', call)
@@ -73,7 +73,7 @@ class CorrectedFilesPage(unittest.TestCase):
def setUp(self):
corrected = [f"sys/file{i}.bin md5" for i in range(12)]
self.reports = [
_report("recalbox", ["es_bios.xml"], hashes_corrected=corrected),
_report("recalbox", ["es_bios.xml"], hashes_updated=corrected),
_report("retropie", ["scriptmodules/a.sh", "scriptmodules/b.sh"],
invalid=["bash -n failed on a.sh"],
withheld=["a size the format has no field for"]),
@@ -154,7 +154,7 @@ class ThePageAddressesMaintainersAsPeers(unittest.TestCase):
from siteexports import change_text
report = _report("batocera", ["batocera-systems"],
hashes_corrected=["a md5", "b md5"], requirements_corrected=3)
hashes_updated=["a md5", "b md5"], required_aligned=3)
report["counts"] = {"kept": 692, "added": 253, "not_added": 10, "not_kept": 0}
self.assertEqual(
change_text(report),
@@ -163,7 +163,7 @@ class ThePageAddressesMaintainersAsPeers(unittest.TestCase):
)
def test_no_wording_that_judges_the_original(self):
report = _report("batocera", ["batocera-systems"], hashes_corrected=["a md5"])
report = _report("batocera", ["batocera-systems"], hashes_updated=["a md5"])
report["counts"] = {"kept": 1, "added": 0, "not_added": 4, "not_kept": 1}
page = generate_exports_page([report], {}).lower()
for word in ("their file", "theirs", "corrected", "wrong", "drift", "left out"):
@@ -243,13 +243,29 @@ class TheRunReportStatesWhatTheFileCarries(unittest.TestCase):
for text in [report["summary"], *messages]:
for word in self.JUDGING:
self.assertNotIn(word, text.lower())
# The report is served as api/v1/exports/<platform>.json, keys and
# change kinds included: "hash_corrected" sat on 130 Recalbox lines.
names = [*report, *(c["kind"] for c in report["changes"])]
self.assertIn("hash_updated", names)
for name in names:
for word in self.JUDGING:
self.assertNotIn(word, name)
def test_the_committed_reports_use_the_same_names(self):
import json # noqa: PLC0415
for path in sorted((ROOT / "exports").glob("*.json")):
report = json.loads(path.read_text(encoding="utf-8"))
names = [*report, *(c["kind"] for c in report.get("changes") or [])]
with self.subTest(report=path.name):
self.assertFalse([n for n in names if "corrected" in n])
UPSTREAM_CACHE = ROOT / ".cache" / "upstream-native"
_DAT_ROM = re.compile(r"^\trom \( name (\"[^\"]+\"|\S+) (.*) \)$")
_DAT_FIELDS = {"size": "size", "crc": "crc32", "md5": "md5", "sha1": "sha1"}
_HASH_KINDS = {
"hash_corrected", "hash_filled", "hash_cleared", "size_corrected", "size_removed",
"hash_updated", "hash_filled", "hash_cleared", "size_updated", "size_removed",
}
@@ -366,7 +382,7 @@ class ReportedChangesAreTheDiff(unittest.TestCase):
((c["system"], path_of[(c["system"], c["name"], c["destination"])]),
c["field"], _norm(c["old"]), _norm(c["new"]))
for c in changes
if c["kind"] in _HASH_KINDS or c["kind"] == "requirement_corrected"
if c["kind"] in _HASH_KINDS or c["kind"] == "required_aligned"
}
self.assertTrue(changed, "nothing changed, the comparison proves nothing")
self.assertEqual(reported, changed)
@@ -420,10 +436,10 @@ class ThePageShowsEachValueBeforeAndAfter(unittest.TestCase):
self.report = _report(
"recalbox", ["es_bios.xml"],
hashes_corrected=["psx/scph5501.bin md5"], requirements_corrected=1,
hashes_updated=["psx/scph5501.bin md5"], required_aligned=1,
changes=[
change("hash_corrected", "md5", "5626" + "0" * 28, "c523" + "1" * 28),
change("requirement_corrected", "required", True, False),
change("hash_updated", "md5", "5626" + "0" * 28, "c523" + "1" * 28),
change("required_aligned", "required", True, False),
change("hash_filled", "sha1", None, "f" * 40),
],
)
+1 -1
View File
@@ -1451,7 +1451,7 @@ class RecalboxExport(unittest.TestCase):
self.assertNotEqual(element.get("mandatory"), "false")
self.assertEqual(
[c for c in landed_changes(exporter, systems).records
if c["kind"] == "requirement_corrected"],
if c["kind"] == "required_aligned"],
[],
)
+16
View File
@@ -470,6 +470,22 @@ class SitePages(unittest.TestCase):
def item(self, **values: object) -> dict:
return {**self.ITEM, **values}
def test_the_data_page_claims_no_link_an_emulator_page_lacks(self) -> None:
"""The Data page said each per-emulator database is "linked from its
page". An emulator page shows the proof lines, which link the record
lines and never the database."""
with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as scratch:
path = Path(scratch) / "m.sqlite"
connection = sqlite3.connect(path)
connection.execute("CREATE TABLE meta (key TEXT)")
connection.commit()
connection.close()
text = "\n".join(sm.data_page_section(path))
proofs = "\n".join(sm.proof_section([self.item()]))
self.assertNotIn("downloads/measurements/", proofs)
self.assertNotIn("linked from its page", text)
self.assertIn("downloads/measurements/<emulator>.sqlite", text)
URL = f"{REPO_URL}/blob/{'e' * 40}/measurements/beetle_psx.jsonl"
def record(self, line: int, proves=(), shown=None, observed=None) -> dict:
+11
View File
@@ -157,6 +157,17 @@ class NoGeneratedTypography(unittest.TestCase):
offenders.append(f"{path.relative_to(ROOT)}:{number}: {line.strip()[:100]}")
self.assertEqual(offenders, [], "\n".join(offenders[:40]))
def test_no_double_hyphen_dash_in_profiles(self):
"""GLYPHS sees U+2014, not its ASCII spelling: "No separate upstream --
written directly for the libretro API" reached the cruzes page as is."""
dash = re.compile(r"[A-Za-z0-9)] -- [A-Za-z0-9(]")
offenders = []
for path in sorted((ROOT / "emulators").glob("*.yml")):
for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
if dash.search(BACKTICKS.sub("", line)):
offenders.append(f"{path.relative_to(ROOT)}:{number}: {line.strip()[:100]}")
self.assertEqual(offenders, [], "\n".join(offenders))
class NoMarketingVocabulary(unittest.TestCase):
"""The project bans these words in published text, and nothing checked
+9
View File
@@ -150,6 +150,15 @@ class WhatASearchSnippetSays(unittest.TestCase):
self.assertTrue(plain.startswith(text[:-3]))
self.assertIn(plain[len(text) - 3], " ,:")
def test_a_link_whose_text_holds_brackets_keeps_its_text_only(self):
"""The Pioneer meta description published the whole link, anchor
included: the text `[BIOS] ...bin` stopped the pattern at its "]"."""
self.assertEqual(
gs._plain_markdown("**Start with** [`[BIOS] LaserActive PAC-N1 (Japan) (v1.02).bin`]"
"(#sha1-6973e2593e66). ares reads it."),
"Start with [BIOS] LaserActive PAC-N1 (Japan) (v1.02).bin. ares reads it.")
self.assertEqual(gs._plain_markdown("[a [b] c](x) and [d](y)"), "a [b] c and d")
def test_code_keeps_its_underscores(self):
self.assertEqual(gs._plain_markdown("only `validate_schemas.py` and _this_"),
"only validate_schemas.py and this")
+38 -1
View File
@@ -87,11 +87,48 @@ class TheGivenProfilesAndPlatforms(_Inputs):
self.assertEqual(sitediagrams.registry()["retroarch"].get("release_pack"), False)
class TheCollectionDoesNotDependOnTheWorkingDirectory(unittest.TestCase):
"""database.json names its bios folder "bios", and the collection model
read it from the working directory: started from tmp/, it found no copy
on disk and dropped five system sections and 2,500 collection paths."""
def test_same_sections_and_paths_from_another_directory(self) -> None:
import json
import sitecollection
db = json.loads((REPO_ROOT / "database.json").read_text(encoding="utf-8"))
here = sitecollection.collection_paths(db)
places = sitecollection._copy_places(db)
elsewhere = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp" if (REPO_ROOT / "tmp").is_dir() else None))
self.addCleanup(shutil.rmtree, elsewhere)
cwd = os.getcwd()
os.chdir(elsewhere)
self.addCleanup(os.chdir, cwd)
self.assertEqual(sitecollection.collection_paths(db), here)
self.assertEqual(sitecollection._copy_places(db), places)
self.assertGreater(sum(map(len, here.values())), len(here))
def test_the_bios_folder_sits_beside_the_given_database(self) -> None:
import siteinputs
siteinputs.use(siteinputs.SiteInputs(db=Path("/elsewhere/database.json")))
self.addCleanup(siteinputs.use, siteinputs.SiteInputs())
self.assertEqual(siteinputs.bios_dir({"bios_dir": "bios"}), Path("/elsewhere/bios"))
self.assertEqual(siteinputs.bios_dir({"bios_dir": "/abs/bios"}), Path("/abs/bios"))
def test_the_image_tools_do_not_load_the_page_generator(self) -> None:
for module in ("make_og_cards.py", "fetch_site_images.py", "siterender.py"):
with self.subTest(module=module):
source = (REPO_ROOT / "scripts" / module).read_text(encoding="utf-8")
self.assertNotIn("from generate_site import", source)
class NoPageModuleReadsTheRepositoryInputs(unittest.TestCase):
"""A helper that opens the repository's database, profiles or platform
files itself bypasses the flags. siteinputs.current() is the one source."""
MODULES = ("generate_site.py", "siterender.py", "sitediagrams.py")
MODULES = ("generate_site.py", "siterender.py", "sitediagrams.py", "sitecollection.py")
INPUTS = {"database.json", "emulators", "platforms"}
def test_no_literal_input_path(self) -> None:
+32
View File
@@ -51,6 +51,25 @@ class TheCounterAsksNothingOfTheDevice(unittest.TestCase):
self.assertIn("countEvent(`search-none/${searchWords(query)}`)", self.code)
self.assertIn("message.startsWith('No results for')", self.code)
def _shape(self, name: str):
"""A search-shape predicate of count.ts, its regexes run by Python
(the counter's patterns use no syntax the two read differently)."""
body = re.search(rf"const {name} = \(query: string\): boolean =>(.*?);\n", self.code, re.S).group(1)
patterns = [re.compile(p) for p in re.findall(r"/(\^.*?\$|\[[^/]*?\]|\\d)/\.test\(query\)", body)]
digits = "(query.match(/\\d/g) ?? []).length <= 4" in body
return lambda q: all(p.search(q) for p in patterns) and (
not digits or len(re.findall(r"\d", q)) <= 4)
def test_a_number_alone_is_never_sent(self) -> None:
"""An 8-digit phone number is valid hex and went out as a hash."""
shapes = [self._shape(n) for n in ("isHash", "isFileName", "isFileStem")]
for query in ("55512345", "91234567", "0612345678", "+33612345678"):
with self.subTest(query=query):
self.assertFalse(any(shape(query) for shape in shapes))
for query in ("b4430a3b", "scph5501", "scph5501.bin", "a" * 40):
with self.subTest(query=query):
self.assertTrue(any(shape(query) for shape in shapes))
def test_one_endpoint(self) -> None:
self.assertEqual(set(re.findall(r"https://[\w.-]+", self.code)),
{"https://retrobios.goatcounter.com"})
@@ -72,6 +91,19 @@ class ThePageSaysWhatIsCounted(unittest.TestCase):
with self.subTest(said=said):
self.assertIn(said, page)
def test_the_page_names_what_the_counter_cannot_tell_apart(self) -> None:
"""The page left out outbound clicks, which count.ts sends, said the
referrer went by domain when the origin goes, and promised a search
word goes out only as a file name while a nickname with digits does."""
page = PRIVACY.read_text(encoding="utf-8")
code = COUNTER.read_text(encoding="utf-8")
self.assertIn("`out/${url.hostname}`", code)
self.assertIn("link to another site", page)
self.assertIn("from.origin", code)
self.assertIn("`https://www.google.com`", page)
self.assertIn("`dupont1985`", page)
self.assertIn("phone number, never is", page)
def test_the_installer_has_no_counter(self) -> None:
installer = (REPO_ROOT / "install.py").read_text(encoding="utf-8")
self.assertNotIn("goatcounter", installer.lower())
+1 -1
View File
@@ -1104,7 +1104,7 @@ class SourceLinksOnlyWhereTheFileIs(unittest.TestCase):
profile = {"source": "https://github.com/o/r", "source_commit": "e" * 40}
text = sitesources._source_ref_markdown(
profile, "Nesoid 2.5.0 libnes.so 0x455a4-0x45694 (setter), 0x8094-0x80ac")
self.assertEqual(text, "offset `0x455a4-0x45694` in `Nesoid 2.5.0 libnes.so`, "
self.assertEqual(text, "offset `0x455a4-0x45694` in `Nesoid 2.5.0 libnes.so` (setter), "
"offset `0x8094-0x80ac` in `Nesoid 2.5.0 libnes.so`")
def test_only_a_recorded_path_links(self):
+41
View File
@@ -257,6 +257,47 @@ class AnAliasTreeIsListedOnItsParentsPage(unittest.TestCase):
self.assertEqual([t["ref"] for t in trees], ["freeintv-overlays"])
class CitationsKeepTheirAnnotations(unittest.TestCase):
"""split_source_ref drops annotations for profile_sync, and the cards
reused it: abuse's two lsf lines lost "(lsf default)" and "(lsf
override)", the only words saying which one wins."""
@mock.patch("sitesources.tree_holds", lambda *_args: True)
def test_each_part_shows_what_the_profile_wrote_beside_it(self):
import sitesources
profile = {"source": "https://github.com/Xenoveritas/abuse",
"source_commit": "a" * 40}
ref = ("src/loader2.cpp:284 (lsf default), src/netcfg.cpp:204 (lsf override), "
"libretro.cpp:1520-1521 candidates_a1200, 30-31 (see <system_dir>), src/x.c:1")
(label, items), = sitesources.source_ref_items(profile, ref)
texts = [text for text, _role in items]
self.assertTrue(texts[0].endswith(") (lsf default)"), texts[0])
self.assertTrue(texts[1].endswith(") (lsf override)"), texts[1])
self.assertTrue(texts[2].endswith(") candidates_a1200"), texts[2])
self.assertTrue(texts[3].endswith(") (see &lt;system_dir>)"), texts[3])
self.assertTrue(texts[4].endswith(")"), texts[4])
def test_the_annotation_is_what_follows_the_location(self):
from sourceref import ref_annotation
self.assertEqual(ref_annotation("munt ROMInfo.cpp:206-213"), "")
self.assertEqual(ref_annotation("source atari800/src/sysrom.c:91"), "")
self.assertEqual(ref_annotation("doc/notes.txt (read at boot)"), "(read at boot)")
self.assertEqual(ref_annotation("a.c (see b.c:12)"), "(see b.c:12)")
self.assertEqual(ref_annotation("0x53c338-0x53c36f (dreamBios row)"), "(dreamBios row)")
def test_a_binary_citation_keeps_its_annotation(self):
import sitesources
(_label, items), = sitesources.source_ref_items(
{}, "demul.exe 0x6b4d80-0x6b4ea3 (dc table), 0x53ca4d-0x53ca7e (load, fatal)")
self.assertEqual([text for text, _ in items], [
"offset `0x6b4d80-0x6b4ea3` in `demul.exe` (dc table)",
"offset `0x53ca4d-0x53ca7e` in `demul.exe` (load, fatal)",
])
class ForgesUpstreamDoesNotRead(unittest.TestCase):
"""Profiles pinned on SourceForge, voidpoint.io, Bitbucket or Software
Heritage showed no revision, and their citations were plain code."""
+8
View File
@@ -2878,11 +2878,19 @@
"github.com/coelckers/prboom-plus@969515162c5aebea4ad7a125ee178dcad3d576ad": [
"prboom2/CMakeLists.txt",
"prboom2/data/CMakeLists.txt",
"prboom2/doc/prboom-plus.6",
"prboom2/src/CMakeLists.txt",
"prboom2/src/MUSIC/flplayer.c",
"prboom2/src/SDL/i_system.c",
"prboom2/src/d_client.c",
"prboom2/src/d_main.c",
"prboom2/src/e6y.c",
"prboom2/src/g_game.c",
"prboom2/src/gl_hires.c",
"prboom2/src/gl_shader.c",
"prboom2/src/m_menu.c",
"prboom2/src/m_misc.c",
"prboom2/src/r_demo.c",
"prboom2/src/w_wad.c"
],
"github.com/crawl/crawl@a5ed93406571b9682050cfc318b15b9f637d4e7e": [
+16 -10
View File
@@ -4,11 +4,12 @@
// not to be tracked (Do Not Track, Global Privacy Control), from a local
// build, or from a page not served at the site's address.
//
// Sent: the page path (never its query or #anchor), the domain of the site
// the reader came from, and the name of an action. An action is a click on
// Sent: the page path (never its query or #anchor), the origin of the site
// the reader came from (scheme and host, no path), and the name of an
// action. An action is a click on
// a copy button, a pack or file download, a link that leaves the site, a
// site search that found nothing (its words only when they are a file
// name or a hash), and the answer to "Did this page help?".
// site search that found nothing (its words only in the shapes below),
// and the answer to "Did this page help?".
const ENDPOINT = 'https://retrobios.goatcounter.com/count';
const HOST = 'abdess.github.io';
@@ -37,7 +38,7 @@ export const countEvent = (name: string): void => {
send({ p: name.slice(0, 120), t: pagePath(), e: 'true' });
};
// The site the reader came from, by domain alone: a path can carry a
// The site the reader came from, by its origin alone: a path can carry a
// search or a private thread.
const referrer = (): string => {
try {
@@ -104,11 +105,16 @@ const onClick = (event: MouseEvent): void => {
// Search
// A search that found nothing names a missing page. Its words are sent
// only in a shape no person's details take: a hash, a file name
// ("scph5501.bin"), or one word of letters and at most four digits
// ("scph5501", "ps3updat"). Any other search is counted with no words,
// as "other": a name, a phone number or an address is never sent.
const isHash = (query: string): boolean => /^(?:[0-9a-f]{8}|[0-9a-f]{32}|[0-9a-f]{40}|[0-9a-f]{64})$/.test(query);
// only in three shapes: a hash with at least one letter, a file name
// ("scph5501.bin"), or one word that starts with a letter and holds at most
// four digits ("scph5501", "ps3updat"). Any other search is counted with no
// words, as "other". A number alone, a phone number among them, is never
// sent. A nickname of the third shape ("dupont1985") is, as wiki/privacy.md
// says: nothing tells it from a file name.
// An 8-digit phone number is valid hex: a hash needs a letter, and a CRC32
// written in digits only is counted as "other".
const isHash = (query: string): boolean =>
/^(?:[0-9a-f]{8}|[0-9a-f]{32}|[0-9a-f]{40}|[0-9a-f]{64})$/.test(query) && /[a-f]/.test(query);
// The extensions of the files the collection holds: "john.smith" is no file.
const isFileName = (query: string): boolean =>
/^[\w.+-]{1,60}\.(?:bin|rom|zip|7z|pup|keys|img|dat|bios|ic\d+|u\d+|sys|nvm|mec|rom1|j64|pce|col|sms|gb|gbc|gba|nds|cfg|chd|iso|txt|fw|xml|yaml|pak|pk3|grp)$/.test(query);
+8 -4
View File
@@ -19,7 +19,8 @@ boot, with no message saying why. It is not a guarantee either: source
profiles can document files nobody has dumped or that only the user can
provide, all of them visible in the [gap analysis](../gaps.md).
From a clone of the repository, `python scripts/generate_pack.py --platform
A clone carries the whole collection, several gigabytes, and `git clone
--depth 1` leaves its history out. From that clone, `python scripts/generate_pack.py --platform
retroarch --region us` builds a narrower pack, `--required-only` the bare
minimum each core needs to start, and `--no-game-data` keeps the firmware and
leaves out the files a game reads. `--help` lists every way to build your own,
@@ -134,9 +135,12 @@ Three rules keep a filtered pack usable:
- **No system is ever emptied.** If nothing matches, the whole group is kept.
`--region us` still ships `disksys.rom`, because the Famicom Disk System has no
American BIOS.
- **Region-free files always survive.** `psxonpsp660.bin`, `ps1_rom.bin` and
`openbios.bin` are declared `[world]`: they add a capability rather than
competing regionally.
- **Region-free files survive.** A file one of the platform's cores declares
`[world]` is kept. SwanStation declares `psxonpsp660.bin` and `ps1_rom.bin`
that way, DuckStation `openbios.bin`. The tag comes from each core:
PCSX ReARMed reads the region inside `psxonpsp660.bin` and files it as
`[japan]`, so on MinUI, NextUI and MyMinUI, where it is the only PlayStation
core, `--region us` drops it.
- **Untagged files always survive.** Most BIOS are not regional and carry no
`region:` at all.
+7 -2
View File
@@ -16,7 +16,8 @@ When a page opens, your browser sends one request to
carries:
- the page address, without the part after `?` or `#`;
- the site you came from, by its domain only (`google.com`, `reddit.com`).
- the site you came from, by its address without a path
(`https://www.google.com`).
GoatCounter reads the browser name and the country from that request. It does
not keep your IP address. To tell a reload from a second visitor it keeps a
@@ -25,9 +26,13 @@ code made from the request for 8 hours, then drops it.
These actions are counted the same way, with the page they happened on:
- a click on a copy button, a pack download or a file link;
- a click on a link to another site, by that site's domain (`reddit.com`);
- a site search that found nothing, so a missing page shows up. Its words are
sent only when they read as a file name (`scph5501.bin`, `scph5501`) or a
hash. Any other search is counted as "other", without its words;
hash with at least one letter. A word of letters with a few digits is sent
as typed, so a nickname written that way (`dupont1985`) would be too. A
number alone, such as a phone number, never is. Any other search is counted
as "other", without its words;
- the answer to "Did this page help?".
Not sent: your screen size, your language, a search that found results.
+22 -3
View File
@@ -42,7 +42,9 @@ tests are reported as skipped, so install them before a pull request
full-screen tests drive `install.py --tui` on a pseudo-terminal and read the
screen back with `pyte`, and fuzz the key decoder with `hypothesis`: both skip
without them (`pip install pyte hypothesis`). `pip install -e ".[dev]"` from the
checkout installs all of these at once, from the `dev` extra of `pyproject.toml`.
checkout installs all of these at once, from the `dev` extra of `pyproject.toml`,
which sets lower bounds only. The contributing guide's checklist reads the exact
versions CI pins from `validate.yml`.
No test framework beyond the standard library `unittest` module.
## Modules at a glance
@@ -377,6 +379,9 @@ to the way the project writes:
line naming a .info, core-info or System.dat no "is correct", "is
accurate", "still declares", "omits" or "missing". The note states what
that file declares and what the code reads.
- No pointer at a review in a comment, a docstring or a test name: a review
number in parentheses, a note file named after one, a test module or class
named after a review. The comment says what the code does.
Formatting stays: bold labels, lists, tables and admonitions make a page
easier to use. When markup shows raw on the site, the rendering is fixed and
@@ -460,8 +465,10 @@ A commit subject is English, 50 characters at most, `type: description` with a
lowercase word after the colon. The types are `feat`, `refactor`, `chore`,
`docs` and `fix`. The message does not use bug, regression, revert, dead code,
broken, wrong, hack, workaround or implement (derived forms included), nor the
marketing vocabulary above, and carries no Co-Authored-By trailer. A body
explains the why when the subject cannot: the scenario that failed, the
marketing vocabulary above, and carries no Co-Authored-By trailer. It says
what the change does, never which review asked for it: "review 3", "audit 12"
or "round 5" with its number points at notes no clone holds and is refused. A
body explains the why when the subject cannot: the scenario that failed, the
upstream `file:line` that decides it, what users will see.
```
@@ -469,6 +476,14 @@ feat: add retrobat platform support
refactor: extract shared utils to common.py
```
`scripts/commit_rules.py` holds these rules. Git does not copy hooks with a
clone, so the commit-msg hook that runs them is set up once per clone:
```bash
printf '#!/bin/sh\nexec python3 scripts/commit_rules.py "$1"\n' > .git/hooks/commit-msg
chmod +x .git/hooks/commit-msg
```
Files are staged by name, never with `git add -A` or `git add .`, which sweep in
deletions and work that is not yours. Generated artifacts (`database.json`,
`retired.json`, `derivable.json`, `install/*.json`, `README.md`, `CONTRIBUTING.md`, `exports/`,
@@ -512,6 +527,10 @@ of main passes, not every commit under it.
Modules that need real artifacts skip themselves when those artifacts are absent,
so `test_pack_integrity` is a no-op in CI (no `dist/`) and a real check locally.
The exporter fidelity cases go the other way: they compare with each platform's
own file, which `python scripts/export_native.py --all --fetch --output-dir
tmp/native` downloads into `.cache/upstream-native/`. CI runs it before the
suite, and a clone without that cache skips them.
Run `python -m unittest discover -s tests -t .` before pushing.
Tests must pass before merge. If a test fails in CI, reproduce locally with:
+5 -3
View File
@@ -817,8 +817,9 @@ hard-links `.cache/large/`, `data/` and the gitignored `bios/` files from the
main checkout. Before each job it is fast-forwarded, or rebased when it holds
unpushed commits, onto `origin/main`. A job that finds anything uncommitted
there stops as `blocked` and cleans nothing. Commits name their files
(`git commit -- <paths>`), and their subjects are checked against the
commit-msg hook when the registry loads. A job that leaves a change no commit
(`git commit -- <paths>`), and their subjects are checked against
`scripts/commit_rules.py`, the rules the commit-msg hook runs, when the
registry loads. A job that leaves a change no commit
rule takes fails.
`--push` sends the batch: rebase on `origin/main` (a conflict keeps the batch
@@ -1356,6 +1357,7 @@ before a release (wiki/release-process.md). Its lines stay in
| `slots.py` | Destinations the platform and a core both claim, and how the pack settles each (`--platform`, `--target`, `--json`, `--strict`, and pipeline step 3b passes the run's own `--target`) |
| `region_audit.py` | Confront every profile `region:` with the regions No-Intro names for the same dump (`--json`, `--db`, `--emulators-dir`). An opinion, never an authority: the code decides, and a disagreement is a line to read in the emulator's source |
| `validate_pr.py` | Validate BIOS files in pull requests, post markdown report |
| `pr_checks.py` | Run the checks `validate.yml` runs on a pull request, in its order, past a failure as CI does, and name each check that failed or whose tool is absent (`--list` prints them) |
| `validate_schemas.py` | Validate the data contracts: schemas and semantic invariants. `--source-only` checks `emulators/` and `platforms/` alone, which is what PR validation runs |
| `generate_event_schema.py` | Write `schemas/install-events.schema.json` from `EVENT_KEYS` in `install.py` (`--check` exits 1 when the file is stale, which `validate_schemas.py` also reports) |
| `generate_installer_doc.py` | Rewrite the generated blocks of `wiki/installer.md` from `install.py`: the tables (options, events, exit codes, keys per route, card markers), the line menu, and the run samples `install.py` prints in a sandbox against the committed `install/*.json`, so a new list rewrites the page (`--check` exits 1 when a block is stale, `--page FILE` writes another copy of the page). Pipeline step 7 |
@@ -1391,7 +1393,7 @@ Further options, by script:
| Script | Options |
|--------|---------|
| `auto_fetch.py` | `--create-issues` prints a GitHub issue body per file still missing. It posts nothing |
| `auto_fetch.py` | `--create-issues` prints a GitHub issue body per platform for the files still missing, with their full hashes. It posts nothing |
| `check_buildbot_system.py` | `--update` refreshes the entries found stale, `--json`, `--registry` |
| `check_freshness.py` | `--cache-dir`, `--native-cache-dir` (originals of the native export) and `--truth-dir` move the caches it reads |
| `check_release.py` | `--tag`, `--sums dist/SHA256SUMS.txt`, `--record` (default `release.json`), `--json` |