14 Commits

Author SHA1 Message Date
Abdessamad Derraz
f1855641c5 docs: add granular pack options to tools reference 2026-03-28 09:29:31 +01:00
Abdessamad Derraz
2666ebd9b7 chore: regenerate readme and database timestamps 2026-03-28 09:24:01 +01:00
Abdessamad Derraz
22a1e7caf4 fix: sync NstDatabase.xml hashes in platform configs 2026-03-28 09:19:37 +01:00
Abdessamad Derraz
67186448a2 fix: verify and add manifests to split packs 2026-03-28 08:43:41 +01:00
Abdessamad Derraz
70891314d3 fix: include core extras in split packs 2026-03-28 08:29:37 +01:00
Abdessamad Derraz
97b9900f62 chore: update NstDatabase.xml from upstream nestopia
Closes #40
2026-03-28 08:24:13 +01:00
Abdessamad Derraz
a1aa97a70e fix: include core extras in split packs 2026-03-28 08:06:22 +01:00
Abdessamad Derraz
97b1c2c08a feat: add --from-md5 lookup and pack builder 2026-03-28 01:02:25 +01:00
Abdessamad Derraz
0624e9d87e test: add validation tests for pack arg combos 2026-03-28 00:59:02 +01:00
Abdessamad Derraz
3ded72f72b feat: add --group-by manufacturer for split packs 2026-03-28 00:45:12 +01:00
Abdessamad Derraz
94a28f5459 feat: add --split flag for per-system packs 2026-03-28 00:43:20 +01:00
Abdessamad Derraz
837ac80cca refactor: deduplicate manufacturer prefix list 2026-03-28 00:39:28 +01:00
Abdessamad Derraz
43cb7a9884 feat: allow --platform + --system combination 2026-03-28 00:36:51 +01:00
Abdessamad Derraz
020ff148c2 feat: add --required-only flag to generate_pack 2026-03-28 00:32:58 +01:00
11 changed files with 1500 additions and 79 deletions

View File

@@ -111,4 +111,4 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
This repository provides BIOS files for personal backup and archival purposes.
*Auto-generated on 2026-03-27T22:52:26Z*
*Auto-generated on 2026-03-28T08:23:13Z*

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml version="1.0" encoding="UTF-8"?>
<database version="1.0" conformance="loose">
<game>
<cartridge system="NES-PAL" dump="ok" crc="001388B3" sha1="4BCD36C05FCAF45C74001257C65AFB7EC5FA53D7">
@@ -69,6 +69,18 @@
</board>
</cartridge>
</game>
<game>
<peripherals>
<device type="fourplayer" />
</peripherals>
<cartridge system="NES-NTSC" dump="ok" crc="00D2CB22" sha1="CCD60DBC65EC004956E972E116BDD114E8818E3E">
<board type="NES-NROM-256" mapper="0">
<prg size="32k" />
<chr size="8k" />
<pad h="1" v="0" />
</board>
</cartridge>
</game>
<game>
<cartridge system="NES-NTSC" dump="ok" crc="00E95D86" sha1="8957393A397DB102BCE5A64B4D85384D1F2E5D20">
<board type="NES-UNROM" mapper="2">
@@ -767,6 +779,15 @@
</board>
</cartridge>
</game>
<game>
<cartridge system="Famicom" dump="unknown" crc="084F61CD" sha1="44BC6C4E8B3F6C635281B4C05382E8F316D8269E">
<board type="NAMCOT-3301" mapper="0">
<prg size="8k" />
<chr size="8k" />
<pad h="0" v="1" />
</board>
</cartridge>
</game>
<game>
<cartridge system="NES-NTSC" dump="ok" crc="085DE7C9" sha1="93339F07696CE1B98F1272613067482A02F47B95">
<board type="NES-SLROM" mapper="1">
@@ -1043,7 +1064,7 @@
<device type="zapper" />
</peripherals>
<cartridge system="Famicom" dump="unknown" crc="0AFB395E" sha1="CFFAC7D2ECB18A28C36E0E90A6682DFE5BA6E3D1">
<board mapper="5">
<board type="HVC-ELROM" mapper="5">
<prg size="128k" />
<chr size="128k" />
</board>
@@ -2018,6 +2039,16 @@
</board>
</cartridge>
</game>
<game>
<peripherals>
<device type="fourplayer" />
</peripherals>
<cartridge system="NES-NTSC" dump="ok" crc="1394DED0" sha1="B1C6A700A9F3B73666018E46515D376F06B8E9C2">
<board type="NES-UNROM" mapper="2">
<prg size="64k" />
</board>
</cartridge>
</game>
<game>
<cartridge system="NES-NTSC" dump="ok" crc="1394F57E" sha1="FD9079CB5E8479EB06D93C2AE5175BFCE871746A">
<board type="NES-SEROM" mapper="1">
@@ -2835,9 +2866,20 @@
</board>
</arcade>
</game>
<game>
<peripherals>
<device type="zapper" />
</peripherals>
<cartridge system="NES-NTSC" dump="unknown" crc="1CA9C322" sha1="17869C4F55461D50E134CC3A4D15B89E7CAF8DE3">
<board mapper="258">
<prg size="128k" />
<chr size="128k" />
</board>
</cartridge>
</game>
<game>
<cartridge system="Famicom" dump="unknown" crc="1CED086F" sha1="46C0B521B3C595409C05972388909CCB0D5F6369">
<board mapper="5">
<board type="HVC-ETROM" mapper="5">
<prg size="256k" />
<chr size="128k" />
<wram size="8k" />
@@ -3086,6 +3128,17 @@
</board>
</cartridge>
</game>
<game>
<peripherals>
<device type="zapper" />
</peripherals>
<cartridge system="NES-NTSC" dump="ok" crc="1F14123C" sha1="9B1B8354B7449FDDA4176C93BCE7660D47F66019">
<board mapper="4">
<prg size="32k" />
<chr size="16k" />
</board>
</cartridge>
</game>
<game>
<cartridge system="Famicom" dump="ok" crc="1F2D9DB7" sha1="544203A8304A7922A46579512665C743527CA1E6">
<board type="HVC-NROM-256" mapper="0">
@@ -4803,6 +4856,17 @@
</board>
</cartridge>
</game>
<game>
<peripherals>
<device type="fourplayer" />
</peripherals>
<cartridge system="NES-NTSC" dump="ok" crc="2DA5ECE0" sha1="F3554E45D3261157653643C23A378C0295A5F893">
<board type="NES-NROM-256" mapper="0">
<prg size="32k" />
<chr size="8k" />
</board>
</cartridge>
</game>
<game>
<cartridge system="Famicom" dump="unknown" crc="2DB7C31E" sha1="9BF95EEB404F103422E06214566C7D918ED4DC79">
<board mapper="1">
@@ -4939,6 +5003,18 @@
</board>
</cartridge>
</game>
<game>
<cartridge system="Famicom" dump="ok" crc="2EA914FA" sha1="54F6E9C7F59D7A1B961F694716F5D4967BB55AA8">
<board type="KONAMI-VRC-4" mapper="25">
<prg size="128k" />
<chr size="128k" />
<chip type="Konami VRC IV">
<pin number="3" function="PRG A2" />
<pin number="4" function="PRG A3" />
</chip>
</board>
</cartridge>
</game>
<game>
<cartridge system="Famicom" dump="unknown" crc="2EBF2E0D" sha1="0E37A2766280D73F2921567348A8D360707A5924">
<board mapper="1">
@@ -5064,6 +5140,20 @@
</board>
</cartridge>
</game>
<game>
<peripherals>
<device type="turbofile" />
</peripherals>
<cartridge system="NES-NTSC" dump="ok" crc="3046C4D5" sha1="1AF51255F2837484974DA44650B186333472C7B2">
<board type="NES-SXROM" mapper="1">
<prg size="512k" />
<vram size="8k" />
<wram size="32k" battery="1" />
<pad h="1" v="0" />
<chip type="MMC1" />
</board>
</cartridge>
</game>
<game>
<cartridge system="NES-PAL" dump="ok" crc="304FA926" sha1="EFFE8CCAA78F94F061B142042557B478B4B213EE">
<board type="NES-NROM-128" mapper="0">
@@ -6706,6 +6796,19 @@
</board>
</cartridge>
</game>
<game>
<peripherals>
<device type="fourplayer" />
</peripherals>
<cartridge system="NES-NTSC" dump="ok" crc="40E1F09E" sha1="2B44E45C621D52C882C50BEF7A2F9B4C93DDF908">
<board mapper="2">
<prg size="64k" />
<vram size="8k" />
<wram size="8k" />
<pad h="1" v="0" />
</board>
</cartridge>
</game>
<game>
<cartridge system="NES-NTSC" dump="ok" crc="40ED2A9D" sha1="D4E9126D02C9923C3871FD352248F41298498D4E">
<board type="NES-SEROM" mapper="1">
@@ -7062,6 +7165,9 @@
</cartridge>
</game>
<game>
<peripherals>
<device type="pachinko" />
</peripherals>
<cartridge system="Famicom" dump="unknown" crc="44F92026" sha1="9266BE2FD5D0C712FE7BF873D32AE50506A9B277">
<board mapper="1">
<prg size="128k" />
@@ -7963,6 +8069,18 @@
</board>
</cartridge>
</game>
<game>
<peripherals>
<device type="fourplayer" />
</peripherals>
<cartridge system="NES-NTSC" dump="ok" crc="4E6B9078" sha1="93FF8CEC778771C7200F785798E0D1599EE8FEB5">
<board type="NES-NROM-256" mapper="0">
<prg size="32k" />
<chr size="8k" />
<pad h="0" v="1" />
</board>
</cartridge>
</game>
<game>
<cartridge system="Famicom" dump="unknown" crc="4E7729FF" sha1="5FA23F88432006DCF6874EA36E9E7DA8934427BE">
<board mapper="182">
@@ -8144,6 +8262,18 @@
</board>
</cartridge>
</game>
<game>
<peripherals>
<device type="fourplayer" />
</peripherals>
<cartridge system="Famicom" dump="ok" crc="4FF17864" sha1="5119F1D6B67C5E44D63BA1E7080A6FE17623415C">
<board type="NES-SLROM" mapper="1">
<prg size="128k" />
<chr size="128k" />
<chip type="MMC1B2" />
</board>
</cartridge>
</game>
<game>
<cartridge system="Famicom" dump="unknown" crc="4FFD17F0" sha1="27CB8AEAF0EA97A6C69D3D90BC056C5EB61695F6">
<board mapper="194">
@@ -8648,6 +8778,15 @@
</board>
</cartridge>
</game>
<game>
<cartridge system="Famicom" dump="ok" crc="54C6FE75" sha1="7C30C244C36C9D1556C79458F06FC46C786028C6">
<board type="JALECO-JF-22-SRAM" mapper="75">
<prg size="256k" />
<chr size="128k" />
<wram size="8k" battery="1" />
</board>
</cartridge>
</game>
<game>
<cartridge system="NES-PAL-B" dump="ok" crc="54E43C57" sha1="1F6072AE901F3D3530ADCD3C136178E3C7354990">
<board type="NES-TLROM" mapper="4">
@@ -8958,6 +9097,19 @@
</board>
</cartridge>
</game>
<game>
<peripherals>
<device type="fourplayer" />
</peripherals>
<cartridge system="NES-NTSC" dump="ok" crc="58094016" sha1="8354E33F44156C42C7013EBBEACF378BFA6FD6B2">
<board mapper="4">
<prg size="64k" />
<chr size="128k" />
<wram size="8k" battery="1" />
<pad h="0" v="1" />
</board>
</cartridge>
</game>
<game>
<cartridge system="NES-NTSC" dump="unknown" crc="58152B42" sha1="1E49BDA9CEF18F6F5C2DA34910487713D364AA68">
<board mapper="79">
@@ -9318,6 +9470,18 @@
</board>
</cartridge>
</game>
<game>
<peripherals>
<device type="fourplayer" />
</peripherals>
<cartridge system="NES-NTSC" dump="ok" crc="5B8D4378" sha1="7194D3DB031C9342BB473A4ADE364DB1631B5EC9">
<board mapper="34">
<prg size="32k" />
<vram size="12k" />
<wram size="8k" battery="1" />
</board>
</cartridge>
</game>
<game>
<cartridge system="NES-NTSC" dump="ok" crc="5BB62688" sha1="D6615439A90FC68758C4149F0CBBE6D1331451F3">
<board type="NES-DEROM" mapper="206">
@@ -10079,6 +10243,16 @@
</board>
</cartridge>
</game>
<game>
<cartridge system="NES-NTSC" dump="ok" crc="633AFE6F" sha1="2F29F3DC724027FAD926BC9D4470A481884E42A5">
<board type="NES-HKROM" mapper="4">
<prg size="32k" />
<chr size="8k" />
<pad h="0" v="1" />
<chip type="MMC6B" battery="0" />
</board>
</cartridge>
</game>
<game>
<cartridge system="Famicom" dump="unknown" crc="63469396" sha1="C6FEF52264372FAB620D1E5EE6A3E60E46262775">
<board mapper="1">
@@ -10126,7 +10300,7 @@
</game>
<game>
<cartridge system="Famicom" dump="unknown" crc="6396B988" sha1="B326D1984D5D369BC168028AD7672D2EFC2ECDDB">
<board mapper="5">
<board type="HVC-ETROM" mapper="5">
<prg size="256k" />
<chr size="128k" />
<wram size="8k" />
@@ -10248,6 +10422,19 @@
</board>
</cartridge>
</game>
<game>
<cartridge system="Famicom" dump="unknown" crc="64AFD592" sha1="618AC6835B96BB5EBFC57DD3B828FAFBB0E0FC7D">
<board type="KONAMI-VRC-4" mapper="23">
<prg size="128k" />
<chr size="128k" />
<wram size="8k" battery="1" />
<chip type="Konami VRC IV">
<pin number="3" function="PRG A3" />
<pin number="4" function="PRG A2" />
</chip>
</board>
</cartridge>
</game>
<game>
<cartridge system="NES-NTSC" dump="ok" crc="64B710D2" sha1="2875F130DAC4C13FFB1D2FDB655A89AED7FEB44A">
<board type="NES-UNROM" mapper="2">
@@ -10833,6 +11020,18 @@
</board>
</cartridge>
</game>
<game>
<peripherals>
<device type="fourplayer" />
</peripherals>
<cartridge system="Famicom" dump="unknown" crc="69977C9E" sha1="C43D5F049F4F7862E6DECCA7500C0C23E349AF9F">
<board mapper="0">
<prg size="32k" />
<chr size="8k" />
<pad h="0" v="1" />
</board>
</cartridge>
</game>
<game>
<cartridge system="NES-NTSC" dump="ok" crc="6997F5E1" sha1="4C8716C4651973B5F6811D6CA9A0F1E2C4E26FA3">
<board type="NES-CNROM" mapper="3">
@@ -11327,7 +11526,7 @@
</game>
<game>
<cartridge system="Famicom" dump="unknown" crc="6F4E4312" sha1="99CF6CA63B173A2B86125F16BBE11885EF1AC377">
<board mapper="5">
<board type="HVC-EWROM" mapper="5">
<prg size="512k" />
<chr size="256k" />
<wram size="32k" battery="1" />
@@ -11744,6 +11943,17 @@
</board>
</cartridge>
</game>
<game>
<peripherals>
<device type="fourplayer" />
</peripherals>
<cartridge system="NES-PAL" dump="ok" crc="73298C87" sha1="16151504C01A3E89C7893A87567B0F31DC651D96">
<board type="PAL-ZZ" mapper="37">
<prg size="256k" />
<chr size="256k" />
</board>
</cartridge>
</game>
<game>
<arcade system="Playchoice-10" dump="unknown" crc="732B0675" sha1="B50E2DCF63E724F3FE8E5ADED50F32AA95775676">
<board mapper="1">
@@ -12063,6 +12273,15 @@
</board>
</cartridge>
</game>
<game>
<cartridge system="Famicom" dump="unknown" crc="76A6A813" sha1="0FE9120FD5ADC2790B0B9E8FADD136F9C66A709F">
<board type="NAMCOT-3301" mapper="0">
<prg size="8k" />
<chr size="8k" />
<pad h="0" v="1" />
</board>
</cartridge>
</game>
<game>
<cartridge system="NES-PAL" dump="ok" crc="76C161E3" sha1="0711BC8D0BF42A0829391C2320393A0D3DF2DD1F">
<board type="NES-SGROM" mapper="1">
@@ -13195,6 +13414,18 @@
</board>
</cartridge>
</game>
<game>
<cartridge system="Famicom" dump="unknown" crc="8281C50F" sha1="DDAB9E627E9F7B7DB068B120CF857D51B2A935C9">
<board type="KONAMI-VRC-4" mapper="23">
<prg size="128k" />
<chr size="128k" />
<chip type="Konami VRC IV">
<pin number="3" function="PRG A3" />
<pin number="4" function="PRG A2" />
</chip>
</board>
</cartridge>
</game>
<game>
<cartridge system="Famicom" dump="unknown" crc="828F8F1F" sha1="9DC376442DB43C7786230AEEB54D5D643A4104E6">
<board mapper="1">
@@ -13580,6 +13811,15 @@
</board>
</cartridge>
</game>
<game>
<cartridge system="Famicom" dump="ok" crc="8589652D" sha1="0CC7ED7F5D7EE0959EE3724C3AF06EF8DF397C59">
<board type="NANJING" mapper="163">
<prg size="2048k" />
<wram size="8k" battery="1" />
<pad h="0" v="1" />
</board>
</cartridge>
</game>
<game>
<cartridge system="NES-NTSC" dump="ok" crc="8593E5AD" sha1="84D2B96C2821FDC246DD876932F4E1752DF1CA73">
<board type="NES-TLROM" mapper="4">
@@ -14365,6 +14605,18 @@
</board>
</cartridge>
</game>
<game>
<peripherals>
<device type="fourplayer" />
</peripherals>
<cartridge system="NES-NTSC" dump="ok" crc="8C29D397" sha1="69612DDE41A2C52A802D9768D7C2942572939867">
<board type="NES-NROM-256" mapper="0">
<prg size="32k" />
<chr size="8k" />
<pad h="0" v="1" />
</board>
</cartridge>
</game>
<game>
<peripherals>
<device type="turbofile" />
@@ -14829,6 +15081,17 @@
</board>
</cartridge>
</game>
<game>
<peripherals>
<device type="fourplayer" />
</peripherals>
<cartridge system="NES-NTSC" dump="ok" crc="90D2E9F0" sha1="2801AADD9D0308CF2C9069A2BB76242ECA5B1501">
<board type="NES-NROM-256" mapper="0">
<prg size="32k" />
<chr size="8k" />
</board>
</cartridge>
</game>
<game>
<cartridge system="NES-NTSC" dump="ok" crc="90D68A43" sha1="7698CE7AE3B83F12518169ECFEEE4D76D643C842">
<board type="NES-CNROM" mapper="3">
@@ -15735,6 +15998,18 @@
</board>
</cartridge>
</game>
<game>
<peripherals>
<device type="fourplayer" />
</peripherals>
<cartridge system="NES-NTSC" dump="ok" crc="9908C6C9" sha1="0A98AB1B7D069A00ACA5D1EB5975C181B2F79E5A">
<board type="NES-CNROM" mapper="3">
<prg size="32k" />
<chr size="32k" />
<pad h="0" v="1" />
</board>
</cartridge>
</game>
<game>
<cartridge system="NES-NTSC" dump="ok" crc="990985C0" sha1="AA08F65D6333448F088D8DCE32F3895662B577DE">
<board type="NES-SLROM" mapper="1">
@@ -15981,6 +16256,9 @@
</cartridge>
</game>
<game>
<peripherals>
<device type="pachinko" />
</peripherals>
<cartridge system="Famicom" dump="unknown" crc="9B3C5124" sha1="A96BFC7B51E2F7FF69F42B024CC9FB85CA9A943D">
<board mapper="4">
<prg size="256k" />
@@ -16633,6 +16911,19 @@
</board>
</cartridge>
</game>
<game>
<peripherals>
<device type="fourplayer" />
</peripherals>
<cartridge system="NES-NTSC" dump="ok" crc="9FC43DD8" sha1="A4A44A54BA682223503BEABE55D5031B5C62B2A7">
<board mapper="34">
<prg size="32k" />
<vram size="8k" />
<wram size="8k" />
<pad h="1" v="0" />
</board>
</cartridge>
</game>
<game>
<cartridge system="Famicom" dump="ok" crc="9FD35802" sha1="C38AF729C2BE2940FCA620F86415FAE304F1D8C9">
<board type="HVC-CNROM" mapper="3">
@@ -17110,6 +17401,16 @@
</board>
</cartridge>
</game>
<game>
<cartridge system="NES-NTSC" dump="ok" crc="A512BDF6" sha1="F794FDA12D34E611D58E652319ED583AE61B81E0">
<board type="NES-HKROM" mapper="4">
<prg size="32k" />
<chr size="8k" />
<pad h="0" v="1" />
<chip type="MMC6B" battery="0" />
</board>
</cartridge>
</game>
<game>
<cartridge system="Famicom" dump="unknown" crc="A531E1AB" sha1="0A0772721642DE00FE575CA109891E274251D815">
<board mapper="4">
@@ -18875,7 +19176,7 @@
</game>
<game>
<cartridge system="Famicom" dump="unknown" crc="B4735FAC" sha1="4AC3E9136706AB009EE2F68C7D009422D73EE8E8">
<board mapper="5">
<board type="HVC-ELROM" mapper="5">
<prg size="512k" />
<chr size="512k" />
</board>
@@ -19569,7 +19870,7 @@
</game>
<game>
<cartridge system="Famicom" dump="unknown" crc="BB7F829A" sha1="BC7393653D04F3E3D35E3D0623ACA4A2C27E0AA1">
<board mapper="5">
<board type= "HVC-ELROM" mapper="5">
<prg size="128k" />
<chr size="128k" />
</board>
@@ -19696,7 +19997,7 @@
</game>
<game>
<cartridge system="Famicom" dump="unknown" crc="BC80FB52" sha1="74DBA27392CA4451875AD0267E5466F92D835A62">
<board mapper="5">
<board type="HVC-EKROM" mapper="5">
<prg size="256k" />
<chr size="256k" />
<wram size="8k" battery="1" />
@@ -19828,6 +20129,15 @@
</board>
</cartridge>
</game>
<game>
<cartridge system="NES-NTSC" dump="unknown" crc="BD75ED79" sha1="2E2B42DA4E0F41F411C778B5A9B66AA9FBD1167E">
<board type="CAMERICA-BF9093" mapper="71">
<prg size="64k" />
<vram size="8k" />
<pad h="1" v="0" />
</board>
</cartridge>
</game>
<game>
<cartridge system="Famicom" dump="ok" crc="BD9D0E85" sha1="F82654BE44E4B01B3F9627D81232A086B1CF7599">
<board type="HVC-UNROM" mapper="2">
@@ -20371,6 +20681,9 @@
</cartridge>
</game>
<game>
<peripherals>
<device type="pachinko" />
</peripherals>
<cartridge system="Famicom" dump="unknown" crc="C22C23AB" sha1="4CEA0ECDF0A22E678B827C9BFD8D80B5DEBB4094">
<board mapper="1">
<prg size="128k" />
@@ -20947,6 +21260,18 @@
</board>
</cartridge>
</game>
<game>
<peripherals>
<device type="pokkunmoguraa" />
</peripherals>
<cartridge system="Famicom" dump="unknown" crc="C7BCC981" sha1="8A0DEADD84A0967B1D2DB0634262C7BDBBB732B7">
<board mapper="3">
<prg size="32k" />
<chr size="32k" />
<pad h="0" v="1" />
</board>
</cartridge>
</game>
<game>
<cartridge system="Dendy" dump="unknown" crc="C7EDBC2E" sha1="E4414C160C7E91136C62D99154336035E5636EEB">
<board mapper="13">
@@ -22458,7 +22783,7 @@
</cartridge>
</game>
<game>
<cartridge system="NES-PAL-B" dump="ok" crc="D67FD6A6" sha1="85DE67A28E01EF680F2FF6AAE80E4315491CEEE9">
<cartridge system="NES-NTSC" dump="ok" crc="D67FD6A6" sha1="85DE67A28E01EF680F2FF6AAE80E4315491CEEE9">
<board type="NES-SNROM" mapper="1">
<prg size="128k" />
<vram size="8k" />
@@ -23635,6 +23960,9 @@
</cartridge>
</game>
<game>
<peripherals>
<device type="pachinko" />
</peripherals>
<cartridge system="Famicom" dump="unknown" crc="E08C8A60" sha1="BAF3A4E0423A86E53234E806843149EF7D0974A9">
<board mapper="4">
<prg size="512k" />
@@ -25207,7 +25535,7 @@
</game>
<game>
<cartridge system="Famicom" dump="unknown" crc="EEE9A682" sha1="46C443D0EB27AF7A566E744F096F981034A06E59">
<board mapper="5">
<board type="HVC-ETROM" mapper="5">
<prg size="256k" />
<chr size="128k" />
<wram size="8k" />
@@ -25492,6 +25820,14 @@
</board>
</cartridge>
</game>
<game>
<cartridge system="NES-NTSC" dump="ok" crc="F312D1DE" sha1="35C157A921156E47FD3F6573D150F54108D0EDFC">
<board type="NES-TXROM" mapper="4">
<prg size="16k" />
<chip type="MMC3A" />
</board>
</cartridge>
</game>
<game>
<cartridge system="NES-NTSC" dump="ok" crc="F31D36A3" sha1="00147962462C44354735861D0258D72314635458">
<board type="NES-TSROM" mapper="4">
@@ -25606,17 +25942,6 @@
</board>
</cartridge>
</game>
<game>
<peripherals>
<device type="fourplayer" />
</peripherals>
<cartridge system="NES-PAL" dump="ok" crc="73298C87" sha1="16151504C01A3E89C7893A87567B0F31DC651D96">
<board type="PAL-ZZ" mapper="37">
<prg size="256k" />
<chr size="256k" />
</board>
</cartridge>
</game>
<game>
<peripherals>
<device type="fourplayer" />
@@ -25698,7 +26023,7 @@
</game>
<game>
<cartridge system="Famicom" dump="unknown" crc="F540677B" sha1="44A5BC2B8156D50518EEBEEFA522A7642E0476DC">
<board mapper="5">
<board type="HVC-EWROM" mapper="5">
<prg size="512k" />
<chr size="256k" />
<wram size="32k" battery="1" />
@@ -26686,6 +27011,16 @@
</board>
</cartridge>
</game>
<game>
<cartridge system="NES-NTSC" dump="ok" crc="FDC7C50B" sha1="5E8F67BEFB2B1BCBD0384E3144ACB2766FC3E443">
<board type="NES-ETROM" mapper="5">
<prg size="128k" />
<chr size="128k" />
<wram size="8k" />
<wram size="8k" battery="1" />
</board>
</cartridge>
</game>
<game>
<cartridge system="Famicom" dump="unknown" crc="FDD89C45" sha1="D25327E0F0D8539AE761DF861254BDE3A60FDD96">
<board mapper="0">
@@ -26767,7 +27102,7 @@
</game>
<game>
<cartridge system="Famicom" dump="unknown" crc="FE3488D1" sha1="800AEFE756E85A0A78CCB4DAE68EBBA5DF24BF41">
<board mapper="5">
<board type="HVC-ETROM" mapper="5">
<prg size="512k" />
<chr size="128k" />
<wram size="8k" />
@@ -26916,4 +27251,15 @@
</board>
</cartridge>
</game>
<!--game>
<peripherals>
<device type="zapper" />
</peripherals>
<cartridge system="Famicom" dump="ok" crc="A0FBF02E" sha1="38236FBD5B70F651674D52EE519AB4DBB11F7955">
<board type="UNL-TF1201" mapper="298">
<prg size="128k" />
<chr size="128k" />
</board>
</cartridge>
</game>-->
</database>

View File

@@ -1,7 +1,7 @@
{
"generated_at": "2026-03-27T22:52:09Z",
"generated_at": "2026-03-28T08:10:17Z",
"total_files": 6756,
"total_size": 5589782999,
"total_size": 5589795834,
"files": {
"520d3d1b5897800af47f92efd2444a26b7a7dead": {
"path": "bios/3DO Company/3DO/3do_arcade_saot.bin",
@@ -33213,15 +33213,15 @@
"crc32": "5205222b",
"adler32": "34389d20"
},
"26322f182540211e9b5e3647675b7c593706ae2b": {
"f92312bae56e29c5bf00a5103105fce78472bf5c": {
"path": "bios/Nintendo/NES/NstDatabase.xml",
"name": "NstDatabase.xml",
"size": 1009534,
"sha1": "26322f182540211e9b5e3647675b7c593706ae2b",
"md5": "7bfe8c0540ed4bd6a0f1e2a0f0118ced",
"sha256": "914584ee6964e8b4cc4ff092874052e7baf13708cfb3f35940342421fcf1bedc",
"crc32": "ebb2196c",
"adler32": "88d01ea2"
"size": 1022369,
"sha1": "f92312bae56e29c5bf00a5103105fce78472bf5c",
"md5": "0ee6cbdc6f5c96ce9c8aa5edb59066f4",
"sha256": "ef5bfd08928b4c9186ba03628ae9f2b7a3b0e30c9204592ac72cd67fa8a31f3a",
"crc32": "0e4d552b",
"adler32": "4f5c1356"
},
"f430a0d752a9fa0c7032db8131f9090d18f71779": {
"path": "bios/Nintendo/NES/gamegenie.nes",
@@ -70887,7 +70887,7 @@
"b953eb1a8fc9922b3f7051c1cdc451f1": "ae7233cae8f94749796e0b740d6021e3b00a8926",
"413154dd0e2c824c9b18b807fd03ec4e": "691e46213d8428befdf568157e670b971ab94e1d",
"c03f6bbaf644eb9b3ee261dbe199eb42": "2faaf92bcaffe675f54f7249d30f3791507e22ab",
"7bfe8c0540ed4bd6a0f1e2a0f0118ced": "26322f182540211e9b5e3647675b7c593706ae2b",
"0ee6cbdc6f5c96ce9c8aa5edb59066f4": "f92312bae56e29c5bf00a5103105fce78472bf5c",
"7f98d77d7a094ad7d069b74bd553ec98": "f430a0d752a9fa0c7032db8131f9090d18f71779",
"aaf3666e4ed478e2964b46d6a7aa27ad": "37027d92e1015b82a7dc5c43e9f1649a961577ab",
"8d3d9f294b6e174bc7b1d2fd1c727530": "bf861922dcb78c316360e3e742f4f70ff63c9bc3",
@@ -83500,7 +83500,7 @@
"2faaf92bcaffe675f54f7249d30f3791507e22ab"
],
"NstDatabase.xml": [
"26322f182540211e9b5e3647675b7c593706ae2b"
"f92312bae56e29c5bf00a5103105fce78472bf5c"
],
"gamegenie.nes": [
"f430a0d752a9fa0c7032db8131f9090d18f71779"
@@ -99007,7 +99007,7 @@
"54c7d10e": "ae7233cae8f94749796e0b740d6021e3b00a8926",
"8bbef508": "691e46213d8428befdf568157e670b971ab94e1d",
"5205222b": "2faaf92bcaffe675f54f7249d30f3791507e22ab",
"ebb2196c": "26322f182540211e9b5e3647675b7c593706ae2b",
"0e4d552b": "f92312bae56e29c5bf00a5103105fce78472bf5c",
"4c514089": "f430a0d752a9fa0c7032db8131f9090d18f71779",
"76f51d6b": "37027d92e1015b82a7dc5c43e9f1649a961577ab",
"7f933ce2": "bf861922dcb78c316360e3e742f4f70ff63c9bc3",

View File

@@ -1325,9 +1325,9 @@ systems:
- name: NstDatabase.xml
destination: NstDatabase.xml
required: true
sha1: 26322f182540211e9b5e3647675b7c593706ae2b
md5: 7bfe8c0540ed4bd6a0f1e2a0f0118ced
crc32: ebb2196c
sha1: f92312bae56e29c5bf00a5103105fce78472bf5c
md5: 0ee6cbdc6f5c96ce9c8aa5edb59066f4
crc32: 0e4d552b
size: 1009534
core: fceumm
manufacturer: Nintendo

View File

@@ -7310,7 +7310,7 @@ systems:
- name: NstDatabase.xml
destination: bios/NstDatabase.xml
required: false
md5: 7bfe8c0540ed4bd6a0f1e2a0f0118ced
md5: 0ee6cbdc6f5c96ce9c8aa5edb59066f4
nintendo-pokemon-mini:
files:
- name: bios.min

View File

@@ -960,8 +960,8 @@ systems:
- name: NstDatabase.xml
destination: nes/NstDatabase.xml
required: true
sha1: 26322f182540211e9b5e3647675b7c593706ae2b
md5: 7bfe8c0540ed4bd6a0f1e2a0f0118ced
sha1: f92312bae56e29c5bf00a5103105fce78472bf5c
md5: 0ee6cbdc6f5c96ce9c8aa5edb59066f4
crc32: ebb2196c
size: 1009534
nintendo-pokemon-mini:

View File

@@ -647,6 +647,29 @@ def resolve_platform_cores(
return result
MANUFACTURER_PREFIXES = (
"microsoft-", "nintendo-", "sony-", "sega-", "snk-", "panasonic-",
"nec-", "epoch-", "mattel-", "fairchild-", "hartung-", "tiger-",
"magnavox-", "philips-", "bandai-", "casio-", "coleco-",
"commodore-", "sharp-", "sinclair-", "atari-",
)
def derive_manufacturer(system_id: str, system_data: dict) -> str:
"""Derive manufacturer name for a system.
Priority: explicit manufacturer field > system ID prefix > 'Other'.
"""
mfr = system_data.get("manufacturer", "")
if mfr and mfr not in ("Various", "Other"):
return mfr.split("|")[0].strip()
s = system_id.lower().replace("_", "-")
for prefix in MANUFACTURER_PREFIXES:
if s.startswith(prefix):
return prefix.rstrip("-").title()
return "Other"
def _norm_system_id(sid: str) -> str:
"""Normalize system ID for cross-platform matching.
@@ -655,11 +678,7 @@ def _norm_system_id(sid: str) -> str:
(e.g., "microsoft-xbox", "nintendo-wii-u").
"""
s = sid.lower().replace("_", "-")
for prefix in ("microsoft-", "nintendo-", "sony-", "sega-",
"snk-", "panasonic-", "nec-", "epoch-", "mattel-",
"fairchild-", "hartung-", "tiger-", "magnavox-",
"philips-", "bandai-", "casio-", "coleco-",
"commodore-", "sharp-", "sinclair-"):
for prefix in MANUFACTURER_PREFIXES:
if s.startswith(prefix):
s = s[len(prefix):]
break
@@ -1068,3 +1087,14 @@ def list_system_ids(emulators_dir: str) -> None:
for sys_id in sorted(system_emus):
count = len(system_emus[sys_id])
print(f" {sys_id:35s} ({count} emulator{'s' if count > 1 else ''})")
def list_platform_system_ids(platform_name: str, platforms_dir: str) -> None:
"""Print system IDs from a platform's YAML config."""
config = load_platform_config(platform_name, platforms_dir)
systems = config.get("systems", {})
for sys_id in sorted(systems):
file_count = len(systems[sys_id].get("files", []))
mfr = systems[sys_id].get("manufacturer", "")
mfr_display = f" [{mfr.split('|')[0]}]" if mfr else ""
print(f" {sys_id:35s} ({file_count} file{'s' if file_count != 1 else ''}){mfr_display}")

View File

@@ -16,6 +16,7 @@ import argparse
import hashlib
import json
import os
import re
import sys
import tempfile
import urllib.request
@@ -25,9 +26,11 @@ from pathlib import Path
sys.path.insert(0, os.path.dirname(__file__))
from common import (
MANUFACTURER_PREFIXES,
_build_validation_index, build_zip_contents_index, check_file_validation,
check_inside_zip, compute_hashes, fetch_large_file, filter_files_by_mode,
group_identical_platforms, list_emulator_profiles, list_registered_platforms,
group_identical_platforms, list_emulator_profiles, list_platform_system_ids,
list_registered_platforms,
filter_systems_by_target, list_system_ids, load_database,
load_data_dir_registry, load_emulator_profiles, load_platform_config,
md5_composite, resolve_local_file,
@@ -46,6 +49,106 @@ DEFAULT_OUTPUT_DIR = "dist"
DEFAULT_BIOS_DIR = "bios"
MAX_ENTRY_SIZE = 512 * 1024 * 1024 # 512MB
_HEX_RE = re.compile(r"\b([0-9a-fA-F]{8,40})\b")
def _detect_hash_type(h: str) -> str:
n = len(h)
if n == 40:
return "sha1"
if n == 32:
return "md5"
if n == 8:
return "crc32"
return "md5"
def parse_hash_input(raw: str) -> list[tuple[str, str]]:
"""Parse comma-separated hash string into (type, hash) tuples."""
results: list[tuple[str, str]] = []
for part in raw.split(","):
part = part.strip().lower()
if not part:
continue
m = _HEX_RE.search(part)
if m:
h = m.group(1)
results.append((_detect_hash_type(h), h))
return results
def parse_hash_file(path: str) -> list[tuple[str, str]]:
"""Parse hash file (one per line, comments with #, mixed formats)."""
results: list[tuple[str, str]] = []
with open(path) as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
m = _HEX_RE.search(line.lower())
if m:
h = m.group(1)
results.append((_detect_hash_type(h), h))
return results
def lookup_hashes(
hashes: list[tuple[str, str]],
db: dict,
bios_dir: str,
emulators_dir: str,
platforms_dir: str,
) -> None:
"""Print diagnostic info for each hash."""
files_db = db.get("files", {})
by_md5 = db.get("indexes", {}).get("by_md5", {})
by_crc32 = db.get("indexes", {}).get("by_crc32", {})
for hash_type, hash_val in hashes:
sha1 = None
if hash_type == "sha1" and hash_val in files_db:
sha1 = hash_val
elif hash_type == "md5":
sha1 = by_md5.get(hash_val)
elif hash_type == "crc32":
sha1 = by_crc32.get(hash_val)
if not sha1 or sha1 not in files_db:
print(f"\n{hash_type.upper()}: {hash_val}")
print(" NOT FOUND in database")
continue
entry = files_db[sha1]
name = entry.get("name", "?")
md5 = entry.get("md5", "?")
paths = entry.get("paths") or []
aliases = entry.get("aliases") or []
print(f"\n{hash_type.upper()}: {hash_val}")
print(f" SHA1: {sha1}")
print(f" MD5: {md5}")
print(f" Name: {name}")
if paths:
print(f" Path: {paths[0]}")
if aliases:
print(f" Aliases: {aliases}")
# Check if file exists in repo (by path or by resolve_local_file)
in_repo = False
if paths:
primary = os.path.join(bios_dir, paths[0])
if os.path.exists(primary):
in_repo = True
if not in_repo:
try:
fe_check = {"name": name, "sha1": sha1, "md5": md5}
local, status = resolve_file(fe_check, db, bios_dir, {})
if local and status != "not_found":
in_repo = True
except (KeyError, OSError):
pass
print(f" In repo: {'YES' if in_repo else 'NO'}")
def _find_candidate_satisfying_both(
file_entry: dict,
@@ -231,6 +334,9 @@ def generate_pack(
data_registry: dict | None = None,
emu_profiles: dict | None = None,
target_cores: set[str] | None = None,
required_only: bool = False,
system_filter: list[str] | None = None,
precomputed_extras: list[dict] | None = None,
) -> str | None:
"""Generate a ZIP pack for a platform.
@@ -246,7 +352,22 @@ def generate_pack(
version = config.get("version", config.get("dat_version", ""))
version_tag = f"_{version.replace(' ', '')}" if version else ""
zip_name = f"{platform_display.replace(' ', '_')}{version_tag}_BIOS_Pack.zip"
req_tag = "_Required" if required_only else ""
sys_tag = ""
if system_filter:
display_parts = []
for sid in system_filter:
s = sid.lower().replace("_", "-")
for prefix in MANUFACTURER_PREFIXES:
if s.startswith(prefix):
s = s[len(prefix):]
break
parts = s.split("-")
display_parts.append("_".join(p.title() for p in parts if p))
sys_tag = "_" + "_".join(display_parts)
zip_name = f"{platform_display.replace(' ', '_')}{version_tag}{req_tag}_BIOS_Pack{sys_tag}.zip"
zip_path = os.path.join(output_dir, zip_name)
os.makedirs(output_dir, exist_ok=True)
@@ -279,9 +400,23 @@ def generate_pack(
platform_cores=plat_cores,
)
if system_filter:
from common import _norm_system_id
norm_filter = {_norm_system_id(s) for s in system_filter}
filtered = {sid: sys_data for sid, sys_data in pack_systems.items()
if sid in system_filter or _norm_system_id(sid) in norm_filter}
if not filtered:
available = sorted(pack_systems.keys())[:10]
print(f" WARNING: no systems matched filter {system_filter} "
f"(available: {', '.join(available)})")
return None
pack_systems = filtered
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
for sys_id, system in sorted(pack_systems.items()):
for file_entry in system.get("files", []):
if required_only and file_entry.get("required") is False:
continue
dest = _sanitize_path(file_entry.get("destination", file_entry["name"]))
if not dest:
# EmuDeck-style entries (system:md5 whitelist, no filename).
@@ -423,12 +558,19 @@ def generate_pack(
# Core requirements: files platform's cores need but YAML doesn't declare
if emu_profiles is None:
emu_profiles = load_emulator_profiles(emulators_dir)
core_files = _collect_emulator_extras(
config, emulators_dir, db,
seen_destinations, base_dest, emu_profiles, target_cores=target_cores,
)
if precomputed_extras is not None:
core_files = precomputed_extras
elif system_filter:
core_files = []
else:
core_files = _collect_emulator_extras(
config, emulators_dir, db,
seen_destinations, base_dest, emu_profiles, target_cores=target_cores,
)
core_count = 0
for fe in core_files:
if required_only and fe.get("required") is False:
continue
dest = _sanitize_path(fe.get("destination", fe["name"]))
if not dest:
continue
@@ -591,6 +733,7 @@ def generate_emulator_pack(
output_dir: str,
standalone: bool = False,
zip_contents: dict | None = None,
required_only: bool = False,
) -> str | None:
"""Generate a ZIP pack for specific emulator profiles."""
all_profiles = load_emulator_profiles(emulators_dir, skip_aliases=False)
@@ -710,6 +853,8 @@ def generate_emulator_pack(
# Pack individual files (skip archived ones)
for fe in files:
if required_only and fe.get("required") is False:
continue
if fe.get("archive"):
continue
@@ -801,6 +946,7 @@ def generate_system_pack(
output_dir: str,
standalone: bool = False,
zip_contents: dict | None = None,
required_only: bool = False,
) -> str | None:
"""Generate a ZIP pack for all emulators supporting given system IDs."""
profiles = load_emulator_profiles(emulators_dir)
@@ -835,7 +981,7 @@ def generate_system_pack(
)
result = generate_emulator_pack(
matching, emulators_dir, db, bios_dir, output_dir,
standalone, zip_contents,
standalone, zip_contents, required_only=required_only,
)
if result:
# Rename to system-based name
@@ -852,6 +998,253 @@ def list_platforms(platforms_dir: str) -> list[str]:
return list_registered_platforms(platforms_dir, include_archived=True)
def _system_display_name(system_id: str) -> str:
"""Convert system ID to display name for ZIP naming."""
s = system_id.lower().replace("_", "-")
for prefix in MANUFACTURER_PREFIXES:
if s.startswith(prefix):
s = s[len(prefix):]
break
parts = s.split("-")
return "_".join(p.title() for p in parts if p)
def _group_systems_by_manufacturer(
systems: dict[str, dict],
db: dict,
bios_dir: str,
) -> dict[str, list[str]]:
"""Group system IDs by manufacturer for --split --group-by manufacturer."""
from common import derive_manufacturer
groups: dict[str, list[str]] = {}
for sid, sys_data in systems.items():
mfr = derive_manufacturer(sid, sys_data)
groups.setdefault(mfr, []).append(sid)
return groups
def generate_split_packs(
platform_name: str,
platforms_dir: str,
db: dict,
bios_dir: str,
output_dir: str,
group_by: str = "system",
emulators_dir: str = "emulators",
zip_contents: dict | None = None,
data_registry: dict | None = None,
emu_profiles: dict | None = None,
target_cores: set[str] | None = None,
required_only: bool = False,
) -> list[str]:
"""Generate split packs (one ZIP per system or manufacturer)."""
config = load_platform_config(platform_name, platforms_dir)
platform_display = config.get("platform", platform_name)
split_dir = os.path.join(output_dir, f"{platform_display.replace(' ', '_')}_Split")
os.makedirs(split_dir, exist_ok=True)
systems = config.get("systems", {})
if group_by == "manufacturer":
groups = _group_systems_by_manufacturer(systems, db, bios_dir)
else:
groups = {_system_display_name(sid): [sid] for sid in systems}
# Pre-compute core extras once (expensive: scans 260+ emulator profiles)
# then distribute per group based on emulator system overlap
if emu_profiles is None:
emu_profiles = load_emulator_profiles(emulators_dir)
base_dest = config.get("base_destination", "")
if emu_profiles:
all_extras = _collect_emulator_extras(
config, emulators_dir, db, set(), base_dest, emu_profiles,
target_cores=target_cores,
)
else:
all_extras = []
# Map each extra to matching systems via source_emulator.
# Index by both profile key AND display name (source_emulator uses display).
from common import _norm_system_id
emu_system_map: dict[str, set[str]] = {}
for name, p in emu_profiles.items():
raw = set(p.get("systems", []))
norm = {_norm_system_id(s) for s in raw}
combined = raw | norm
emu_system_map[name] = combined
display = p.get("emulator", "")
if display and display != name:
emu_system_map[display] = combined
plat_norm = {_norm_system_id(s): s for s in systems}
results = []
for group_name, group_system_ids in sorted(groups.items()):
group_sys_set = set(group_system_ids)
group_norm = {_norm_system_id(s) for s in group_system_ids}
group_match = group_sys_set | group_norm
group_extras = [
fe for fe in all_extras
if emu_system_map.get(fe.get("source_emulator", ""), set()) & group_match
]
zip_path = generate_pack(
platform_name, platforms_dir, db, bios_dir, split_dir,
emulators_dir=emulators_dir, zip_contents=zip_contents,
data_registry=data_registry, emu_profiles=emu_profiles,
target_cores=target_cores, required_only=required_only,
system_filter=group_system_ids, precomputed_extras=group_extras,
)
if zip_path:
version = config.get("version", config.get("dat_version", ""))
ver_tag = f"_{version.replace(' ', '')}" if version else ""
req_tag = "_Required" if required_only else ""
safe_group = group_name.replace(" ", "_")
new_name = f"{platform_display.replace(' ', '_')}{ver_tag}{req_tag}_{safe_group}_BIOS_Pack.zip"
new_path = os.path.join(split_dir, new_name)
if new_path != zip_path:
os.rename(zip_path, new_path)
zip_path = new_path
results.append(zip_path)
# Warn about extras that couldn't be distributed (emulators without systems: field)
all_groups_match = set()
for group_system_ids in groups.values():
group_norm = {_norm_system_id(s) for s in group_system_ids}
all_groups_match |= set(group_system_ids) | group_norm
undistributed = [
fe for fe in all_extras
if not emu_system_map.get(fe.get("source_emulator", ""), set()) & all_groups_match
]
if undistributed:
emus = sorted({fe.get("source_emulator", "?") for fe in undistributed})
print(f" NOTE: {len(undistributed)} core extras from {len(emus)} emulators "
f"not in split packs (missing systems: field in profiles: "
f"{', '.join(emus[:5])}{'...' if len(emus) > 5 else ''})")
return results
def generate_md5_pack(
hashes: list[tuple[str, str]],
db: dict,
bios_dir: str,
output_dir: str,
zip_contents: dict | None = None,
platform_name: str | None = None,
platforms_dir: str | None = None,
emulator_name: str | None = None,
emulators_dir: str | None = None,
standalone: bool = False,
) -> str | None:
"""Build a pack from an explicit list of hashes with layout context."""
files_db = db.get("files", {})
by_md5 = db.get("indexes", {}).get("by_md5", {})
by_crc32 = db.get("indexes", {}).get("by_crc32", {})
if zip_contents is None:
zip_contents = {}
plat_file_index: dict[str, dict] = {}
base_dest = ""
plat_display = "Custom"
if platform_name and platforms_dir:
config = load_platform_config(platform_name, platforms_dir)
base_dest = config.get("base_destination", "")
plat_display = config.get("platform", platform_name)
for _sys_id, system in config.get("systems", {}).items():
for fe in system.get("files", []):
plat_file_index[fe.get("name", "").lower()] = fe
emu_pack_structure = None
emu_display = ""
if emulator_name and emulators_dir:
profiles = load_emulator_profiles(emulators_dir, skip_aliases=False)
if emulator_name in profiles:
profile = profiles[emulator_name]
emu_display = profile.get("emulator", emulator_name)
emu_pack_structure = profile.get("pack_structure")
for fe in profile.get("files", []):
plat_file_index[fe.get("name", "").lower()] = fe
for alias in fe.get("aliases", []):
plat_file_index[alias.lower()] = fe
context_name = plat_display if platform_name else (emu_display or "Custom")
zip_name = f"{context_name.replace(' ', '_')}_Custom_BIOS_Pack.zip"
zip_path = os.path.join(output_dir, zip_name)
os.makedirs(output_dir, exist_ok=True)
packed: list[tuple[str, str]] = []
not_in_repo: list[tuple[str, str]] = []
not_in_db: list[str] = []
seen: set[str] = set()
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
for hash_type, hash_val in hashes:
sha1 = None
if hash_type == "sha1" and hash_val in files_db:
sha1 = hash_val
elif hash_type == "md5":
sha1 = by_md5.get(hash_val)
elif hash_type == "crc32":
sha1 = by_crc32.get(hash_val)
if not sha1 or sha1 not in files_db:
not_in_db.append(hash_val)
continue
entry = files_db[sha1]
name = entry.get("name", "")
aliases = entry.get("aliases") or []
paths = entry.get("paths") or []
dest = name
matched_fe = None
for lookup_name in [name] + aliases:
if lookup_name.lower() in plat_file_index:
matched_fe = plat_file_index[lookup_name.lower()]
break
if matched_fe:
if emulator_name and emu_pack_structure is not None:
dest = _resolve_destination(matched_fe, emu_pack_structure, standalone)
else:
dest = matched_fe.get("destination", matched_fe.get("name", name))
elif paths:
dest = paths[0]
if base_dest and not dest.startswith(base_dest):
full_dest = f"{base_dest}/{dest}"
else:
full_dest = dest
if full_dest in seen:
continue
seen.add(full_dest)
fe_for_resolve = {"name": name, "sha1": sha1, "md5": entry.get("md5", "")}
local_path, status = resolve_file(fe_for_resolve, db, bios_dir, zip_contents)
if status == "not_found" or not local_path:
not_in_repo.append((name, hash_val))
continue
zf.write(local_path, full_dest)
packed.append((name, hash_val))
total = len(hashes)
print(f"\nPacked {len(packed)}/{total} requested files")
for name, h in packed:
print(f" PACKED: {name} ({h[:16]}...)")
for name, h in not_in_repo:
print(f" NOT IN REPO: {name} ({h[:16]}...)")
for h in not_in_db:
print(f" NOT IN DB: {h}")
if not packed:
if os.path.exists(zip_path):
os.unlink(zip_path)
return None
return zip_path
def main():
parser = argparse.ArgumentParser(description="Generate platform BIOS ZIP packs")
parser.add_argument("--platform", "-p", help="Platform name (e.g., retroarch)")
@@ -874,8 +1267,19 @@ def main():
parser.add_argument("--refresh-data", action="store_true",
help="Force re-download all data directories")
parser.add_argument("--list", action="store_true", help="List available platforms")
parser.add_argument("--required-only", action="store_true",
help="Only include required files, skip optional")
parser.add_argument("--split", action="store_true",
help="Generate one ZIP per system/manufacturer")
parser.add_argument("--group-by", choices=["system", "manufacturer"],
default="system",
help="Grouping for --split (default: system)")
parser.add_argument("--target", "-t", help="Hardware target (e.g., switch, rpi4)")
parser.add_argument("--list-targets", action="store_true", help="List available targets for the platform")
parser.add_argument("--from-md5",
help="Hash(es) to look up or pack (comma-separated)")
parser.add_argument("--from-md5-file",
help="File with hashes (one per line)")
args = parser.parse_args()
if args.list:
@@ -887,7 +1291,10 @@ def main():
list_emulator_profiles(args.emulators_dir)
return
if args.list_systems:
list_system_ids(args.emulators_dir)
if args.platform:
list_platform_system_ids(args.platform, args.platforms_dir)
else:
list_system_ids(args.emulators_dir)
return
if args.list_targets:
if not args.platform:
@@ -902,18 +1309,68 @@ def main():
print(f" {t['name']:30s} {t['architecture']:10s} {t['core_count']:>4d} cores{aliases}")
return
# Mutual exclusion
modes = sum(1 for x in (args.platform, args.all, args.emulator, args.system) if x)
if modes == 0:
parser.error("Specify --platform, --all, --emulator, or --system")
if modes > 1:
parser.error("--platform, --all, --emulator, and --system are mutually exclusive")
if args.standalone and not (args.emulator or args.system):
parser.error("--standalone requires --emulator or --system")
if args.target and not (args.platform or args.all):
# Mode validation
has_platform = bool(args.platform)
has_all = args.all
has_emulator = bool(args.emulator)
has_system = bool(args.system)
has_from_md5 = bool(args.from_md5 or getattr(args, 'from_md5_file', None))
if args.from_md5 and getattr(args, 'from_md5_file', None):
parser.error("--from-md5 and --from-md5-file are mutually exclusive")
if has_from_md5 and has_all:
parser.error("--from-md5 requires --platform or --emulator, not --all")
if has_from_md5 and has_system:
parser.error("--from-md5 and --system are mutually exclusive")
if has_from_md5 and args.split:
parser.error("--split and --from-md5 are mutually exclusive")
# --platform/--all and --system can combine (system filters within platform)
# --emulator is exclusive with everything else
if has_emulator and (has_platform or has_all or has_system):
parser.error("--emulator is mutually exclusive with --platform, --all, and --system")
if has_platform and has_all:
parser.error("--platform and --all are mutually exclusive")
if not (has_platform or has_all or has_emulator or has_system or has_from_md5):
parser.error("Specify --platform, --all, --emulator, --system, or --from-md5")
if args.standalone and not (has_emulator or (has_system and not has_platform and not has_all)):
parser.error("--standalone requires --emulator or --system (without --platform)")
if args.split and not (has_platform or has_all):
parser.error("--split requires --platform or --all")
if args.split and has_emulator:
parser.error("--split is incompatible with --emulator")
if args.group_by != "system" and not args.split:
parser.error("--group-by requires --split")
if args.target and not (has_platform or has_all):
parser.error("--target requires --platform or --all")
if args.target and (args.emulator or args.system):
parser.error("--target is incompatible with --emulator and --system")
if args.target and has_emulator:
parser.error("--target is incompatible with --emulator")
# Hash lookup / pack mode
if has_from_md5:
if args.from_md5:
hashes = parse_hash_input(args.from_md5)
else:
hashes = parse_hash_file(args.from_md5_file)
if not hashes:
print("No valid hashes found in input", file=sys.stderr)
sys.exit(1)
db = load_database(args.db)
if not has_platform and not has_emulator:
lookup_hashes(hashes, db, args.bios_dir, args.emulators_dir,
args.platforms_dir)
return
zip_contents = build_zip_contents_index(db)
result = generate_md5_pack(
hashes=hashes, db=db, bios_dir=args.bios_dir,
output_dir=args.output_dir, zip_contents=zip_contents,
platform_name=args.platform, platforms_dir=args.platforms_dir,
emulator_name=args.emulator, emulators_dir=args.emulators_dir,
standalone=getattr(args, "standalone", False),
)
if not result:
sys.exit(1)
return
db = load_database(args.db)
zip_contents = build_zip_contents_index(db)
@@ -923,23 +1380,27 @@ def main():
names = [n.strip() for n in args.emulator.split(",") if n.strip()]
result = generate_emulator_pack(
names, args.emulators_dir, db, args.bios_dir, args.output_dir,
args.standalone, zip_contents,
args.standalone, zip_contents, required_only=args.required_only,
)
if not result:
sys.exit(1)
return
# System mode
if args.system:
# System mode (standalone, without platform context)
if has_system and not has_platform and not has_all:
system_ids = [s.strip() for s in args.system.split(",") if s.strip()]
result = generate_system_pack(
system_ids, args.emulators_dir, db, args.bios_dir, args.output_dir,
args.standalone, zip_contents,
args.standalone, zip_contents, required_only=args.required_only,
)
if not result:
sys.exit(1)
return
system_filter = None
if args.system:
system_filter = [s.strip() for s in args.system.split(",") if s.strip()]
# Platform mode (existing)
if args.all:
platforms = list_registered_platforms(
@@ -998,13 +1459,25 @@ def main():
try:
tc = target_cores_cache.get(representative) if args.target else None
zip_path = generate_pack(
representative, args.platforms_dir, db, args.bios_dir, args.output_dir,
include_extras=args.include_extras, emulators_dir=args.emulators_dir,
zip_contents=zip_contents, data_registry=data_registry,
emu_profiles=emu_profiles, target_cores=tc,
)
if zip_path and variants:
if args.split:
zip_paths = generate_split_packs(
representative, args.platforms_dir, db, args.bios_dir,
args.output_dir, group_by=args.group_by,
emulators_dir=args.emulators_dir, zip_contents=zip_contents,
data_registry=data_registry, emu_profiles=emu_profiles,
target_cores=tc, required_only=args.required_only,
)
print(f" Split into {len(zip_paths)} packs")
else:
zip_path = generate_pack(
representative, args.platforms_dir, db, args.bios_dir, args.output_dir,
include_extras=args.include_extras, emulators_dir=args.emulators_dir,
zip_contents=zip_contents, data_registry=data_registry,
emu_profiles=emu_profiles, target_cores=tc,
required_only=args.required_only,
system_filter=system_filter,
)
if not args.split and zip_path and variants:
rep_cfg = load_platform_config(representative, args.platforms_dir)
ver = rep_cfg.get("version", rep_cfg.get("dat_version", ""))
ver_tag = f"_{ver.replace(' ', '')}" if ver else ""
@@ -1020,7 +1493,17 @@ def main():
# Post-generation: verify all packs + inject manifests + SHA256SUMS
if not args.list_emulators and not args.list_systems:
print("\nVerifying packs and generating manifests...")
all_ok = verify_and_finalize_packs(args.output_dir, db)
# Skip platform conformance for filtered/split/custom packs
skip_conf = bool(system_filter or args.split)
all_ok = verify_and_finalize_packs(args.output_dir, db,
skip_conformance=skip_conf)
# Also verify split subdirectories
if args.split:
for entry in os.listdir(args.output_dir):
sub = os.path.join(args.output_dir, entry)
if os.path.isdir(sub) and entry.endswith("_Split"):
ok = verify_and_finalize_packs(sub, db, skip_conformance=True)
all_ok = all_ok and ok
if not all_ok:
print("WARNING: some packs have verification errors")
sys.exit(1)
@@ -1267,7 +1750,8 @@ def verify_pack_against_platform(
def verify_and_finalize_packs(output_dir: str, db: dict,
platforms_dir: str = "platforms") -> bool:
platforms_dir: str = "platforms",
skip_conformance: bool = False) -> bool:
"""Verify all packs, inject manifests, generate SHA256SUMS.
Two-stage verification:
@@ -1307,6 +1791,9 @@ def verify_and_finalize_packs(output_dir: str, db: dict,
inject_manifest(zip_path, manifest)
# Stage 2: platform conformance (extract + verify)
# Skipped for filtered/split/custom packs (intentionally partial)
if skip_conformance:
continue
platforms = pack_to_platform.get(name, [])
for pname in platforms:
(p_ok, total, matched, p_errors,

View File

@@ -1604,10 +1604,22 @@ def generate_wiki_tools() -> str:
"Build platform-specific BIOS ZIP packs.",
"",
"```bash",
"# Full platform packs",
"python scripts/generate_pack.py --all --output-dir dist/",
"python scripts/generate_pack.py --platform batocera",
"python scripts/generate_pack.py --emulator dolphin",
"python scripts/generate_pack.py --system atari-lynx",
"",
"# Granular options",
"python scripts/generate_pack.py --platform retroarch --system sony-playstation",
"python scripts/generate_pack.py --platform batocera --required-only",
"python scripts/generate_pack.py --platform retroarch --split",
"python scripts/generate_pack.py --platform retroarch --split --group-by manufacturer",
"",
"# Hash-based lookup and custom packs",
"python scripts/generate_pack.py --from-md5 d8f1206299c48946e6ec5ef96d014eaa",
"python scripts/generate_pack.py --platform batocera --from-md5-file missing.txt",
"python scripts/generate_pack.py --platform retroarch --list-systems",
"```",
"",
"Packs include platform baseline files plus files required by the platform's cores.",
@@ -1615,6 +1627,15 @@ def generate_wiki_tools() -> str:
"the tool searches for a variant that satisfies both.",
"If none exists, the platform version is kept and the discrepancy is reported.",
"",
"**Granular options:**",
"",
"- `--system` with `--platform`: filter to specific systems within a platform pack",
"- `--required-only`: exclude optional files, keep only required",
"- `--split`: generate one ZIP per system instead of one big pack",
"- `--split --group-by manufacturer`: group split packs by manufacturer (Sony, Nintendo, Sega...)",
"- `--from-md5`: look up a hash in the database, or build a custom pack with `--platform`/`--emulator`",
"- `--from-md5-file`: same, reading hashes from a file (one per line, comments with #)",
"",
"### cross_reference.py",
"",
"Compare emulator profiles against platform configs.",

View File

@@ -1539,6 +1539,522 @@ class TestE2E(unittest.TestCase):
self.assertEqual(gt["total"], result["total_files"])
self.assertGreaterEqual(gt["with_validation"], 1)
def test_130_required_only_excludes_optional(self):
"""--required-only excludes files with required: false from pack."""
from generate_pack import generate_pack
output_dir = os.path.join(self.root, "pack_reqonly")
os.makedirs(output_dir, exist_ok=True)
# Create a platform with one required and one optional file
config = {
"platform": "ReqOnlyTest",
"verification_mode": "existence",
"base_destination": "system",
"systems": {
"test-sys": {
"files": [
{"name": "present_req.bin", "destination": "present_req.bin", "required": True},
{"name": "present_opt.bin", "destination": "present_opt.bin", "required": False},
],
},
},
}
with open(os.path.join(self.platforms_dir, "test_reqonly.yml"), "w") as fh:
yaml.dump(config, fh)
zip_path = generate_pack(
"test_reqonly", self.platforms_dir, self.db, self.bios_dir, output_dir,
required_only=True,
)
self.assertIsNotNone(zip_path)
with zipfile.ZipFile(zip_path) as zf:
names = zf.namelist()
self.assertTrue(any("present_req.bin" in n for n in names))
self.assertFalse(any("present_opt.bin" in n for n in names))
# Verify _Required tag in filename
self.assertIn("_Required_", os.path.basename(zip_path))
def test_131_required_only_keeps_default_required(self):
"""--required-only keeps files with no required field (default = required)."""
from generate_pack import generate_pack
output_dir = os.path.join(self.root, "pack_reqdef")
os.makedirs(output_dir, exist_ok=True)
# File with no required field
config = {
"platform": "ReqDefTest",
"verification_mode": "existence",
"base_destination": "system",
"systems": {
"test-sys": {
"files": [
{"name": "present_req.bin", "destination": "present_req.bin"},
],
},
},
}
with open(os.path.join(self.platforms_dir, "test_reqdef.yml"), "w") as fh:
yaml.dump(config, fh)
zip_path = generate_pack(
"test_reqdef", self.platforms_dir, self.db, self.bios_dir, output_dir,
required_only=True,
)
self.assertIsNotNone(zip_path)
with zipfile.ZipFile(zip_path) as zf:
names = zf.namelist()
self.assertTrue(any("present_req.bin" in n for n in names))
def test_132_platform_system_filter(self):
"""--platform + --system filters systems within a platform pack."""
from generate_pack import generate_pack
output_dir = os.path.join(self.root, "pack_sysfilter")
os.makedirs(output_dir, exist_ok=True)
config = {
"platform": "SysFilterTest",
"verification_mode": "existence",
"base_destination": "system",
"systems": {
"system-a": {
"files": [
{"name": "present_req.bin", "destination": "present_req.bin"},
],
},
"system-b": {
"files": [
{"name": "present_opt.bin", "destination": "present_opt.bin"},
],
},
},
}
with open(os.path.join(self.platforms_dir, "test_sysfilter.yml"), "w") as fh:
yaml.dump(config, fh)
zip_path = generate_pack(
"test_sysfilter", self.platforms_dir, self.db, self.bios_dir, output_dir,
system_filter=["system-a"],
)
self.assertIsNotNone(zip_path)
with zipfile.ZipFile(zip_path) as zf:
names = zf.namelist()
self.assertTrue(any("present_req.bin" in n for n in names))
self.assertFalse(any("present_opt.bin" in n for n in names))
def test_133_platform_system_filter_normalized(self):
"""_norm_system_id normalization matches with manufacturer prefix."""
from common import _norm_system_id
self.assertEqual(
_norm_system_id("sony-playstation"),
_norm_system_id("playstation"),
)
def test_134_list_systems_platform_context(self):
"""list_platform_system_ids lists systems from a platform YAML."""
from common import list_platform_system_ids
import io
config = {
"platform": "ListSysTest",
"verification_mode": "existence",
"systems": {
"alpha-sys": {
"files": [
{"name": "a.bin", "destination": "a.bin"},
],
},
"beta-sys": {
"files": [
{"name": "b1.bin", "destination": "b1.bin"},
{"name": "b2.bin", "destination": "b2.bin"},
],
},
},
}
with open(os.path.join(self.platforms_dir, "test_listsys.yml"), "w") as fh:
yaml.dump(config, fh)
captured = io.StringIO()
old_stdout = sys.stdout
sys.stdout = captured
try:
list_platform_system_ids("test_listsys", self.platforms_dir)
finally:
sys.stdout = old_stdout
output = captured.getvalue()
self.assertIn("alpha-sys", output)
self.assertIn("beta-sys", output)
self.assertIn("1 file", output)
self.assertIn("2 files", output)
def test_135_split_by_system(self):
"""--split generates one ZIP per system in a subdirectory."""
import tempfile
with tempfile.TemporaryDirectory() as tmpdir:
plat_dir = os.path.join(tmpdir, "platforms")
os.makedirs(plat_dir)
bios_dir = os.path.join(tmpdir, "bios", "Test")
os.makedirs(os.path.join(bios_dir, "SysA"))
os.makedirs(os.path.join(bios_dir, "SysB"))
emu_dir = os.path.join(tmpdir, "emulators")
os.makedirs(emu_dir)
out_dir = os.path.join(tmpdir, "dist")
file_a = os.path.join(bios_dir, "SysA", "bios_a.bin")
file_b = os.path.join(bios_dir, "SysB", "bios_b.bin")
with open(file_a, "wb") as f:
f.write(b"system_a")
with open(file_b, "wb") as f:
f.write(b"system_b")
from common import compute_hashes
ha = compute_hashes(file_a)
hb = compute_hashes(file_b)
db = {
"files": {
ha["sha1"]: {"name": "bios_a.bin", "md5": ha["md5"],
"sha1": ha["sha1"], "sha256": ha["sha256"],
"path": file_a,
"paths": [file_a]},
hb["sha1"]: {"name": "bios_b.bin", "md5": hb["md5"],
"sha1": hb["sha1"], "sha256": hb["sha256"],
"path": file_b,
"paths": [file_b]},
},
"indexes": {
"by_md5": {ha["md5"]: ha["sha1"], hb["md5"]: hb["sha1"]},
"by_name": {"bios_a.bin": [ha["sha1"]], "bios_b.bin": [hb["sha1"]]},
"by_crc32": {}, "by_path_suffix": {},
},
}
registry = {"platforms": {"splitplat": {"status": "active"}}}
with open(os.path.join(plat_dir, "_registry.yml"), "w") as f:
yaml.dump(registry, f)
plat_cfg = {
"platform": "SplitTest",
"verification_mode": "existence",
"systems": {
"test-system-a": {"files": [{"name": "bios_a.bin", "sha1": ha["sha1"]}]},
"test-system-b": {"files": [{"name": "bios_b.bin", "sha1": hb["sha1"]}]},
},
}
with open(os.path.join(plat_dir, "splitplat.yml"), "w") as f:
yaml.dump(plat_cfg, f)
from generate_pack import generate_split_packs
from common import build_zip_contents_index, load_emulator_profiles
zip_contents = build_zip_contents_index(db)
emu_profiles = load_emulator_profiles(emu_dir)
zip_paths = generate_split_packs(
"splitplat", plat_dir, db, os.path.join(tmpdir, "bios"), out_dir,
emulators_dir=emu_dir, zip_contents=zip_contents,
emu_profiles=emu_profiles, group_by="system",
)
self.assertEqual(len(zip_paths), 2)
# Check subdirectory exists
split_dir = os.path.join(out_dir, "SplitTest_Split")
self.assertTrue(os.path.isdir(split_dir))
# Verify each ZIP contains only its system's files
for zp in zip_paths:
with zipfile.ZipFile(zp) as zf:
names = zf.namelist()
basename = os.path.basename(zp)
if "System_A" in basename:
self.assertIn("bios_a.bin", names)
self.assertNotIn("bios_b.bin", names)
elif "System_B" in basename:
self.assertIn("bios_b.bin", names)
self.assertNotIn("bios_a.bin", names)
def test_136_derive_manufacturer(self):
"""derive_manufacturer extracts manufacturer correctly."""
from common import derive_manufacturer
# From system ID prefix
self.assertEqual(derive_manufacturer("sony-playstation", {}), "Sony")
self.assertEqual(derive_manufacturer("nintendo-snes", {}), "Nintendo")
self.assertEqual(derive_manufacturer("sega-saturn", {}), "Sega")
self.assertEqual(derive_manufacturer("atari-5200", {}), "Atari")
# From explicit manufacturer field
self.assertEqual(
derive_manufacturer("3do", {"manufacturer": "Panasonic|GoldStar"}),
"Panasonic",
)
# Various = skip to prefix check, then Other
self.assertEqual(derive_manufacturer("arcade", {"manufacturer": "Various"}), "Other")
# Fallback
self.assertEqual(derive_manufacturer("dos", {}), "Other")
def test_137_group_systems_by_manufacturer(self):
"""_group_systems_by_manufacturer groups correctly."""
from generate_pack import _group_systems_by_manufacturer
systems = {
"sony-playstation": {"files": [{"name": "a.bin"}]},
"sony-psp": {"files": [{"name": "b.bin"}]},
"nintendo-snes": {"files": [{"name": "c.bin"}]},
"arcade": {"manufacturer": "Various", "files": [{"name": "d.bin"}]},
}
groups = _group_systems_by_manufacturer(systems, {}, "")
self.assertIn("Sony", groups)
self.assertEqual(sorted(groups["Sony"]), ["sony-playstation", "sony-psp"])
self.assertIn("Nintendo", groups)
self.assertEqual(groups["Nintendo"], ["nintendo-snes"])
self.assertIn("Other", groups)
self.assertEqual(groups["Other"], ["arcade"])
def test_138_parse_hash_input(self):
"""parse_hash_input handles various formats."""
from generate_pack import parse_hash_input
# Plain MD5
result = parse_hash_input("d8f1206299c48946e6ec5ef96d014eaa")
self.assertEqual(result, [("md5", "d8f1206299c48946e6ec5ef96d014eaa")])
# Comma-separated
result = parse_hash_input("d8f1206299c48946e6ec5ef96d014eaa,d8f1206299c48946e6ec5ef96d014eab")
self.assertEqual(len(result), 2)
# SHA1
sha1 = "a" * 40
result = parse_hash_input(sha1)
self.assertEqual(result, [("sha1", sha1)])
# CRC32
result = parse_hash_input("abcd1234")
self.assertEqual(result, [("crc32", "abcd1234")])
def test_139_parse_hash_file(self):
"""parse_hash_file handles comments, empty lines, various formats."""
from generate_pack import parse_hash_file
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
f.write("# PS1 BIOS files\n")
f.write("\n")
f.write("d8f1206299c48946e6ec5ef96d014eaa\n")
f.write("d8f1206299c48946e6ec5ef96d014eab scph5501.bin\n")
f.write("scph5502.bin d8f1206299c48946e6ec5ef96d014eac OK\n")
tmp_path = f.name
try:
result = parse_hash_file(tmp_path)
self.assertEqual(len(result), 3)
self.assertTrue(all(t == "md5" for t, _ in result))
finally:
os.unlink(tmp_path)
def test_140_lookup_hashes_found(self):
"""lookup_hashes returns file info for known hashes."""
import io
import contextlib
from generate_pack import lookup_hashes
db = {
"files": {
"sha1abc": {
"name": "test.bin", "md5": "md5abc",
"sha1": "sha1abc", "sha256": "sha256abc",
"paths": ["Mfr/Console/test.bin"],
"aliases": ["alt.bin"],
},
},
"indexes": {
"by_md5": {"md5abc": "sha1abc"},
"by_crc32": {},
},
}
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
lookup_hashes([("md5", "md5abc")], db, "bios", "emulators", "platforms")
output = buf.getvalue()
self.assertIn("test.bin", output)
self.assertIn("sha1abc", output)
self.assertIn("alt.bin", output)
def test_141_lookup_hashes_not_found(self):
"""lookup_hashes reports unknown hashes."""
import io
import contextlib
from generate_pack import lookup_hashes
db = {"files": {}, "indexes": {"by_md5": {}, "by_crc32": {}}}
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
lookup_hashes([("md5", "unknown123" + "0" * 22)], db, "bios", "emulators", "platforms")
output = buf.getvalue()
self.assertIn("NOT FOUND", output)
def test_142_from_md5_platform_pack(self):
"""--from-md5 with --platform generates correctly laid out ZIP."""
import tempfile
import json
import zipfile
import yaml
with tempfile.TemporaryDirectory() as tmpdir:
plat_dir = os.path.join(tmpdir, "platforms")
os.makedirs(plat_dir)
bios_dir = os.path.join(tmpdir, "bios", "Sony", "PS1")
os.makedirs(bios_dir)
emu_dir = os.path.join(tmpdir, "emulators")
os.makedirs(emu_dir)
out_dir = os.path.join(tmpdir, "dist")
bios_file = os.path.join(bios_dir, "scph5501.bin")
with open(bios_file, "wb") as f:
f.write(b"ps1_bios_content")
from common import compute_hashes
h = compute_hashes(bios_file)
db = {
"files": {
h["sha1"]: {
"name": "scph5501.bin", "md5": h["md5"],
"sha1": h["sha1"], "sha256": h["sha256"],
"path": bios_file,
"paths": ["Sony/PS1/scph5501.bin"],
},
},
"indexes": {
"by_md5": {h["md5"]: h["sha1"]},
"by_name": {"scph5501.bin": [h["sha1"]]},
"by_crc32": {}, "by_path_suffix": {},
},
}
registry = {"platforms": {"testplat": {"status": "active"}}}
with open(os.path.join(plat_dir, "_registry.yml"), "w") as f:
yaml.dump(registry, f)
plat_cfg = {
"platform": "TestPlat",
"verification_mode": "md5",
"base_destination": "bios",
"systems": {
"sony-playstation": {
"files": [
{"name": "scph5501.bin", "md5": h["md5"],
"destination": "scph5501.bin"},
]
}
},
}
with open(os.path.join(plat_dir, "testplat.yml"), "w") as f:
yaml.dump(plat_cfg, f)
from generate_pack import generate_md5_pack
from common import build_zip_contents_index
zip_contents = build_zip_contents_index(db)
zip_path = generate_md5_pack(
hashes=[("md5", h["md5"])],
db=db, bios_dir=bios_dir, output_dir=out_dir,
zip_contents=zip_contents,
platform_name="testplat", platforms_dir=plat_dir,
)
self.assertIsNotNone(zip_path)
with zipfile.ZipFile(zip_path) as zf:
names = zf.namelist()
self.assertIn("bios/scph5501.bin", names)
self.assertIn("Custom", os.path.basename(zip_path))
def test_143_from_md5_not_in_repo(self):
"""--from-md5 reports files in DB but missing from repo."""
import tempfile
import io
import contextlib
from generate_pack import generate_md5_pack
db = {
"files": {
"sha1known": {
"name": "missing.bin", "md5": "md5known" + "0" * 25,
"sha1": "sha1known", "sha256": "sha256known",
"path": "/nonexistent/missing.bin",
"paths": ["Test/missing.bin"],
},
},
"indexes": {
"by_md5": {"md5known" + "0" * 25: "sha1known"},
"by_crc32": {},
},
}
with tempfile.TemporaryDirectory() as tmpdir:
out_dir = os.path.join(tmpdir, "dist")
bios_dir = os.path.join(tmpdir, "bios")
os.makedirs(bios_dir)
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
result = generate_md5_pack(
hashes=[("md5", "md5known" + "0" * 25)],
db=db, bios_dir=bios_dir, output_dir=out_dir,
zip_contents={},
)
output = buf.getvalue()
self.assertIn("NOT IN REPO", output)
self.assertIsNone(result)
def test_144_invalid_split_emulator(self):
"""--split + --emulator is rejected."""
import subprocess
result = subprocess.run(
["python", "scripts/generate_pack.py", "--emulator", "test", "--split"],
capture_output=True, text=True,
)
self.assertNotEqual(result.returncode, 0)
self.assertIn("error", result.stderr.lower())
def test_145_invalid_from_md5_all(self):
"""--from-md5 + --all is rejected."""
import subprocess
result = subprocess.run(
["python", "scripts/generate_pack.py", "--all", "--from-md5", "abc123" + "0" * 26],
capture_output=True, text=True,
)
self.assertNotEqual(result.returncode, 0)
def test_146_invalid_from_md5_system(self):
"""--from-md5 + --system is rejected."""
import subprocess
result = subprocess.run(
["python", "scripts/generate_pack.py", "--system", "psx", "--from-md5", "abc123" + "0" * 26],
capture_output=True, text=True,
)
self.assertNotEqual(result.returncode, 0)
def test_147_invalid_group_by_without_split(self):
"""--group-by without --split is rejected."""
import subprocess
result = subprocess.run(
["python", "scripts/generate_pack.py", "--platform", "retroarch", "--group-by", "manufacturer"],
capture_output=True, text=True,
)
self.assertNotEqual(result.returncode, 0)
def test_148_valid_platform_system(self):
"""--platform + --system is accepted (not rejected at validation stage)."""
import argparse
sys.path.insert(0, "scripts")
# Build the same parser as generate_pack.main()
parser = argparse.ArgumentParser()
parser.add_argument("--platform", "-p")
parser.add_argument("--all", action="store_true")
parser.add_argument("--emulator", "-e")
parser.add_argument("--system", "-s")
parser.add_argument("--standalone", action="store_true")
parser.add_argument("--split", action="store_true")
parser.add_argument("--group-by", choices=["system", "manufacturer"], default="system")
parser.add_argument("--target", "-t")
parser.add_argument("--from-md5")
parser.add_argument("--from-md5-file")
parser.add_argument("--required-only", action="store_true")
args = parser.parse_args(["--platform", "retroarch", "--system", "psx"])
# Replicate validation logic from main()
has_platform = bool(args.platform)
has_all = args.all
has_emulator = bool(args.emulator)
has_system = bool(args.system)
has_from_md5 = bool(args.from_md5 or args.from_md5_file)
# These should NOT raise
self.assertFalse(has_emulator and (has_platform or has_all or has_system))
self.assertFalse(has_platform and has_all)
self.assertTrue(has_platform or has_all or has_emulator or has_system or has_from_md5)
# --platform + --system is a valid combination
self.assertTrue(has_platform and has_system)
if __name__ == "__main__":
unittest.main()

View File

@@ -51,10 +51,22 @@ Verification modes per platform:
Build platform-specific BIOS ZIP packs.
```bash
# Full platform packs
python scripts/generate_pack.py --all --output-dir dist/
python scripts/generate_pack.py --platform batocera
python scripts/generate_pack.py --emulator dolphin
python scripts/generate_pack.py --system atari-lynx
# Granular options
python scripts/generate_pack.py --platform retroarch --system sony-playstation
python scripts/generate_pack.py --platform batocera --required-only
python scripts/generate_pack.py --platform retroarch --split
python scripts/generate_pack.py --platform retroarch --split --group-by manufacturer
# Hash-based lookup and custom packs
python scripts/generate_pack.py --from-md5 d8f1206299c48946e6ec5ef96d014eaa
python scripts/generate_pack.py --platform batocera --from-md5-file missing.txt
python scripts/generate_pack.py --platform retroarch --list-systems
```
Packs include platform baseline files plus files required by the platform's cores.
@@ -62,6 +74,15 @@ When a file passes platform verification but fails emulator validation,
the tool searches for a variant that satisfies both.
If none exists, the platform version is kept and the discrepancy is reported.
**Granular options:**
- `--system` with `--platform`: filter to specific systems within a platform pack
- `--required-only`: exclude optional files, keep only required
- `--split`: generate one ZIP per system instead of one big pack
- `--split --group-by manufacturer`: group split packs by manufacturer (Sony, Nintendo, Sega...)
- `--from-md5`: look up a hash in the database, or build a custom pack with `--platform`/`--emulator`
- `--from-md5-file`: same, reading hashes from a file (one per line, comments with #)
### cross_reference.py
Compare emulator profiles against platform configs.