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
+1 -1
View File
@@ -111,4 +111,4 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
This repository provides BIOS files for personal backup and archival purposes. 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*
+369 -23
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"> <database version="1.0" conformance="loose">
<game> <game>
<cartridge system="NES-PAL" dump="ok" crc="001388B3" sha1="4BCD36C05FCAF45C74001257C65AFB7EC5FA53D7"> <cartridge system="NES-PAL" dump="ok" crc="001388B3" sha1="4BCD36C05FCAF45C74001257C65AFB7EC5FA53D7">
@@ -69,6 +69,18 @@
</board> </board>
</cartridge> </cartridge>
</game> </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> <game>
<cartridge system="NES-NTSC" dump="ok" crc="00E95D86" sha1="8957393A397DB102BCE5A64B4D85384D1F2E5D20"> <cartridge system="NES-NTSC" dump="ok" crc="00E95D86" sha1="8957393A397DB102BCE5A64B4D85384D1F2E5D20">
<board type="NES-UNROM" mapper="2"> <board type="NES-UNROM" mapper="2">
@@ -767,6 +779,15 @@
</board> </board>
</cartridge> </cartridge>
</game> </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> <game>
<cartridge system="NES-NTSC" dump="ok" crc="085DE7C9" sha1="93339F07696CE1B98F1272613067482A02F47B95"> <cartridge system="NES-NTSC" dump="ok" crc="085DE7C9" sha1="93339F07696CE1B98F1272613067482A02F47B95">
<board type="NES-SLROM" mapper="1"> <board type="NES-SLROM" mapper="1">
@@ -1043,7 +1064,7 @@
<device type="zapper" /> <device type="zapper" />
</peripherals> </peripherals>
<cartridge system="Famicom" dump="unknown" crc="0AFB395E" sha1="CFFAC7D2ECB18A28C36E0E90A6682DFE5BA6E3D1"> <cartridge system="Famicom" dump="unknown" crc="0AFB395E" sha1="CFFAC7D2ECB18A28C36E0E90A6682DFE5BA6E3D1">
<board mapper="5"> <board type="HVC-ELROM" mapper="5">
<prg size="128k" /> <prg size="128k" />
<chr size="128k" /> <chr size="128k" />
</board> </board>
@@ -2018,6 +2039,16 @@
</board> </board>
</cartridge> </cartridge>
</game> </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> <game>
<cartridge system="NES-NTSC" dump="ok" crc="1394F57E" sha1="FD9079CB5E8479EB06D93C2AE5175BFCE871746A"> <cartridge system="NES-NTSC" dump="ok" crc="1394F57E" sha1="FD9079CB5E8479EB06D93C2AE5175BFCE871746A">
<board type="NES-SEROM" mapper="1"> <board type="NES-SEROM" mapper="1">
@@ -2835,9 +2866,20 @@
</board> </board>
</arcade> </arcade>
</game> </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> <game>
<cartridge system="Famicom" dump="unknown" crc="1CED086F" sha1="46C0B521B3C595409C05972388909CCB0D5F6369"> <cartridge system="Famicom" dump="unknown" crc="1CED086F" sha1="46C0B521B3C595409C05972388909CCB0D5F6369">
<board mapper="5"> <board type="HVC-ETROM" mapper="5">
<prg size="256k" /> <prg size="256k" />
<chr size="128k" /> <chr size="128k" />
<wram size="8k" /> <wram size="8k" />
@@ -3086,6 +3128,17 @@
</board> </board>
</cartridge> </cartridge>
</game> </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> <game>
<cartridge system="Famicom" dump="ok" crc="1F2D9DB7" sha1="544203A8304A7922A46579512665C743527CA1E6"> <cartridge system="Famicom" dump="ok" crc="1F2D9DB7" sha1="544203A8304A7922A46579512665C743527CA1E6">
<board type="HVC-NROM-256" mapper="0"> <board type="HVC-NROM-256" mapper="0">
@@ -4803,6 +4856,17 @@
</board> </board>
</cartridge> </cartridge>
</game> </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> <game>
<cartridge system="Famicom" dump="unknown" crc="2DB7C31E" sha1="9BF95EEB404F103422E06214566C7D918ED4DC79"> <cartridge system="Famicom" dump="unknown" crc="2DB7C31E" sha1="9BF95EEB404F103422E06214566C7D918ED4DC79">
<board mapper="1"> <board mapper="1">
@@ -4939,6 +5003,18 @@
</board> </board>
</cartridge> </cartridge>
</game> </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> <game>
<cartridge system="Famicom" dump="unknown" crc="2EBF2E0D" sha1="0E37A2766280D73F2921567348A8D360707A5924"> <cartridge system="Famicom" dump="unknown" crc="2EBF2E0D" sha1="0E37A2766280D73F2921567348A8D360707A5924">
<board mapper="1"> <board mapper="1">
@@ -5064,6 +5140,20 @@
</board> </board>
</cartridge> </cartridge>
</game> </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> <game>
<cartridge system="NES-PAL" dump="ok" crc="304FA926" sha1="EFFE8CCAA78F94F061B142042557B478B4B213EE"> <cartridge system="NES-PAL" dump="ok" crc="304FA926" sha1="EFFE8CCAA78F94F061B142042557B478B4B213EE">
<board type="NES-NROM-128" mapper="0"> <board type="NES-NROM-128" mapper="0">
@@ -6706,6 +6796,19 @@
</board> </board>
</cartridge> </cartridge>
</game> </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> <game>
<cartridge system="NES-NTSC" dump="ok" crc="40ED2A9D" sha1="D4E9126D02C9923C3871FD352248F41298498D4E"> <cartridge system="NES-NTSC" dump="ok" crc="40ED2A9D" sha1="D4E9126D02C9923C3871FD352248F41298498D4E">
<board type="NES-SEROM" mapper="1"> <board type="NES-SEROM" mapper="1">
@@ -7062,6 +7165,9 @@
</cartridge> </cartridge>
</game> </game>
<game> <game>
<peripherals>
<device type="pachinko" />
</peripherals>
<cartridge system="Famicom" dump="unknown" crc="44F92026" sha1="9266BE2FD5D0C712FE7BF873D32AE50506A9B277"> <cartridge system="Famicom" dump="unknown" crc="44F92026" sha1="9266BE2FD5D0C712FE7BF873D32AE50506A9B277">
<board mapper="1"> <board mapper="1">
<prg size="128k" /> <prg size="128k" />
@@ -7963,6 +8069,18 @@
</board> </board>
</cartridge> </cartridge>
</game> </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> <game>
<cartridge system="Famicom" dump="unknown" crc="4E7729FF" sha1="5FA23F88432006DCF6874EA36E9E7DA8934427BE"> <cartridge system="Famicom" dump="unknown" crc="4E7729FF" sha1="5FA23F88432006DCF6874EA36E9E7DA8934427BE">
<board mapper="182"> <board mapper="182">
@@ -8144,6 +8262,18 @@
</board> </board>
</cartridge> </cartridge>
</game> </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> <game>
<cartridge system="Famicom" dump="unknown" crc="4FFD17F0" sha1="27CB8AEAF0EA97A6C69D3D90BC056C5EB61695F6"> <cartridge system="Famicom" dump="unknown" crc="4FFD17F0" sha1="27CB8AEAF0EA97A6C69D3D90BC056C5EB61695F6">
<board mapper="194"> <board mapper="194">
@@ -8648,6 +8778,15 @@
</board> </board>
</cartridge> </cartridge>
</game> </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> <game>
<cartridge system="NES-PAL-B" dump="ok" crc="54E43C57" sha1="1F6072AE901F3D3530ADCD3C136178E3C7354990"> <cartridge system="NES-PAL-B" dump="ok" crc="54E43C57" sha1="1F6072AE901F3D3530ADCD3C136178E3C7354990">
<board type="NES-TLROM" mapper="4"> <board type="NES-TLROM" mapper="4">
@@ -8958,6 +9097,19 @@
</board> </board>
</cartridge> </cartridge>
</game> </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> <game>
<cartridge system="NES-NTSC" dump="unknown" crc="58152B42" sha1="1E49BDA9CEF18F6F5C2DA34910487713D364AA68"> <cartridge system="NES-NTSC" dump="unknown" crc="58152B42" sha1="1E49BDA9CEF18F6F5C2DA34910487713D364AA68">
<board mapper="79"> <board mapper="79">
@@ -9318,6 +9470,18 @@
</board> </board>
</cartridge> </cartridge>
</game> </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> <game>
<cartridge system="NES-NTSC" dump="ok" crc="5BB62688" sha1="D6615439A90FC68758C4149F0CBBE6D1331451F3"> <cartridge system="NES-NTSC" dump="ok" crc="5BB62688" sha1="D6615439A90FC68758C4149F0CBBE6D1331451F3">
<board type="NES-DEROM" mapper="206"> <board type="NES-DEROM" mapper="206">
@@ -10079,6 +10243,16 @@
</board> </board>
</cartridge> </cartridge>
</game> </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> <game>
<cartridge system="Famicom" dump="unknown" crc="63469396" sha1="C6FEF52264372FAB620D1E5EE6A3E60E46262775"> <cartridge system="Famicom" dump="unknown" crc="63469396" sha1="C6FEF52264372FAB620D1E5EE6A3E60E46262775">
<board mapper="1"> <board mapper="1">
@@ -10126,7 +10300,7 @@
</game> </game>
<game> <game>
<cartridge system="Famicom" dump="unknown" crc="6396B988" sha1="B326D1984D5D369BC168028AD7672D2EFC2ECDDB"> <cartridge system="Famicom" dump="unknown" crc="6396B988" sha1="B326D1984D5D369BC168028AD7672D2EFC2ECDDB">
<board mapper="5"> <board type="HVC-ETROM" mapper="5">
<prg size="256k" /> <prg size="256k" />
<chr size="128k" /> <chr size="128k" />
<wram size="8k" /> <wram size="8k" />
@@ -10248,6 +10422,19 @@
</board> </board>
</cartridge> </cartridge>
</game> </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> <game>
<cartridge system="NES-NTSC" dump="ok" crc="64B710D2" sha1="2875F130DAC4C13FFB1D2FDB655A89AED7FEB44A"> <cartridge system="NES-NTSC" dump="ok" crc="64B710D2" sha1="2875F130DAC4C13FFB1D2FDB655A89AED7FEB44A">
<board type="NES-UNROM" mapper="2"> <board type="NES-UNROM" mapper="2">
@@ -10833,6 +11020,18 @@
</board> </board>
</cartridge> </cartridge>
</game> </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> <game>
<cartridge system="NES-NTSC" dump="ok" crc="6997F5E1" sha1="4C8716C4651973B5F6811D6CA9A0F1E2C4E26FA3"> <cartridge system="NES-NTSC" dump="ok" crc="6997F5E1" sha1="4C8716C4651973B5F6811D6CA9A0F1E2C4E26FA3">
<board type="NES-CNROM" mapper="3"> <board type="NES-CNROM" mapper="3">
@@ -11327,7 +11526,7 @@
</game> </game>
<game> <game>
<cartridge system="Famicom" dump="unknown" crc="6F4E4312" sha1="99CF6CA63B173A2B86125F16BBE11885EF1AC377"> <cartridge system="Famicom" dump="unknown" crc="6F4E4312" sha1="99CF6CA63B173A2B86125F16BBE11885EF1AC377">
<board mapper="5"> <board type="HVC-EWROM" mapper="5">
<prg size="512k" /> <prg size="512k" />
<chr size="256k" /> <chr size="256k" />
<wram size="32k" battery="1" /> <wram size="32k" battery="1" />
@@ -11744,6 +11943,17 @@
</board> </board>
</cartridge> </cartridge>
</game> </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> <game>
<arcade system="Playchoice-10" dump="unknown" crc="732B0675" sha1="B50E2DCF63E724F3FE8E5ADED50F32AA95775676"> <arcade system="Playchoice-10" dump="unknown" crc="732B0675" sha1="B50E2DCF63E724F3FE8E5ADED50F32AA95775676">
<board mapper="1"> <board mapper="1">
@@ -12063,6 +12273,15 @@
</board> </board>
</cartridge> </cartridge>
</game> </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> <game>
<cartridge system="NES-PAL" dump="ok" crc="76C161E3" sha1="0711BC8D0BF42A0829391C2320393A0D3DF2DD1F"> <cartridge system="NES-PAL" dump="ok" crc="76C161E3" sha1="0711BC8D0BF42A0829391C2320393A0D3DF2DD1F">
<board type="NES-SGROM" mapper="1"> <board type="NES-SGROM" mapper="1">
@@ -13195,6 +13414,18 @@
</board> </board>
</cartridge> </cartridge>
</game> </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> <game>
<cartridge system="Famicom" dump="unknown" crc="828F8F1F" sha1="9DC376442DB43C7786230AEEB54D5D643A4104E6"> <cartridge system="Famicom" dump="unknown" crc="828F8F1F" sha1="9DC376442DB43C7786230AEEB54D5D643A4104E6">
<board mapper="1"> <board mapper="1">
@@ -13580,6 +13811,15 @@
</board> </board>
</cartridge> </cartridge>
</game> </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> <game>
<cartridge system="NES-NTSC" dump="ok" crc="8593E5AD" sha1="84D2B96C2821FDC246DD876932F4E1752DF1CA73"> <cartridge system="NES-NTSC" dump="ok" crc="8593E5AD" sha1="84D2B96C2821FDC246DD876932F4E1752DF1CA73">
<board type="NES-TLROM" mapper="4"> <board type="NES-TLROM" mapper="4">
@@ -14365,6 +14605,18 @@
</board> </board>
</cartridge> </cartridge>
</game> </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> <game>
<peripherals> <peripherals>
<device type="turbofile" /> <device type="turbofile" />
@@ -14829,6 +15081,17 @@
</board> </board>
</cartridge> </cartridge>
</game> </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> <game>
<cartridge system="NES-NTSC" dump="ok" crc="90D68A43" sha1="7698CE7AE3B83F12518169ECFEEE4D76D643C842"> <cartridge system="NES-NTSC" dump="ok" crc="90D68A43" sha1="7698CE7AE3B83F12518169ECFEEE4D76D643C842">
<board type="NES-CNROM" mapper="3"> <board type="NES-CNROM" mapper="3">
@@ -15735,6 +15998,18 @@
</board> </board>
</cartridge> </cartridge>
</game> </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> <game>
<cartridge system="NES-NTSC" dump="ok" crc="990985C0" sha1="AA08F65D6333448F088D8DCE32F3895662B577DE"> <cartridge system="NES-NTSC" dump="ok" crc="990985C0" sha1="AA08F65D6333448F088D8DCE32F3895662B577DE">
<board type="NES-SLROM" mapper="1"> <board type="NES-SLROM" mapper="1">
@@ -15981,6 +16256,9 @@
</cartridge> </cartridge>
</game> </game>
<game> <game>
<peripherals>
<device type="pachinko" />
</peripherals>
<cartridge system="Famicom" dump="unknown" crc="9B3C5124" sha1="A96BFC7B51E2F7FF69F42B024CC9FB85CA9A943D"> <cartridge system="Famicom" dump="unknown" crc="9B3C5124" sha1="A96BFC7B51E2F7FF69F42B024CC9FB85CA9A943D">
<board mapper="4"> <board mapper="4">
<prg size="256k" /> <prg size="256k" />
@@ -16633,6 +16911,19 @@
</board> </board>
</cartridge> </cartridge>
</game> </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> <game>
<cartridge system="Famicom" dump="ok" crc="9FD35802" sha1="C38AF729C2BE2940FCA620F86415FAE304F1D8C9"> <cartridge system="Famicom" dump="ok" crc="9FD35802" sha1="C38AF729C2BE2940FCA620F86415FAE304F1D8C9">
<board type="HVC-CNROM" mapper="3"> <board type="HVC-CNROM" mapper="3">
@@ -17110,6 +17401,16 @@
</board> </board>
</cartridge> </cartridge>
</game> </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> <game>
<cartridge system="Famicom" dump="unknown" crc="A531E1AB" sha1="0A0772721642DE00FE575CA109891E274251D815"> <cartridge system="Famicom" dump="unknown" crc="A531E1AB" sha1="0A0772721642DE00FE575CA109891E274251D815">
<board mapper="4"> <board mapper="4">
@@ -18875,7 +19176,7 @@
</game> </game>
<game> <game>
<cartridge system="Famicom" dump="unknown" crc="B4735FAC" sha1="4AC3E9136706AB009EE2F68C7D009422D73EE8E8"> <cartridge system="Famicom" dump="unknown" crc="B4735FAC" sha1="4AC3E9136706AB009EE2F68C7D009422D73EE8E8">
<board mapper="5"> <board type="HVC-ELROM" mapper="5">
<prg size="512k" /> <prg size="512k" />
<chr size="512k" /> <chr size="512k" />
</board> </board>
@@ -19569,7 +19870,7 @@
</game> </game>
<game> <game>
<cartridge system="Famicom" dump="unknown" crc="BB7F829A" sha1="BC7393653D04F3E3D35E3D0623ACA4A2C27E0AA1"> <cartridge system="Famicom" dump="unknown" crc="BB7F829A" sha1="BC7393653D04F3E3D35E3D0623ACA4A2C27E0AA1">
<board mapper="5"> <board type= "HVC-ELROM" mapper="5">
<prg size="128k" /> <prg size="128k" />
<chr size="128k" /> <chr size="128k" />
</board> </board>
@@ -19696,7 +19997,7 @@
</game> </game>
<game> <game>
<cartridge system="Famicom" dump="unknown" crc="BC80FB52" sha1="74DBA27392CA4451875AD0267E5466F92D835A62"> <cartridge system="Famicom" dump="unknown" crc="BC80FB52" sha1="74DBA27392CA4451875AD0267E5466F92D835A62">
<board mapper="5"> <board type="HVC-EKROM" mapper="5">
<prg size="256k" /> <prg size="256k" />
<chr size="256k" /> <chr size="256k" />
<wram size="8k" battery="1" /> <wram size="8k" battery="1" />
@@ -19828,6 +20129,15 @@
</board> </board>
</cartridge> </cartridge>
</game> </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> <game>
<cartridge system="Famicom" dump="ok" crc="BD9D0E85" sha1="F82654BE44E4B01B3F9627D81232A086B1CF7599"> <cartridge system="Famicom" dump="ok" crc="BD9D0E85" sha1="F82654BE44E4B01B3F9627D81232A086B1CF7599">
<board type="HVC-UNROM" mapper="2"> <board type="HVC-UNROM" mapper="2">
@@ -20371,6 +20681,9 @@
</cartridge> </cartridge>
</game> </game>
<game> <game>
<peripherals>
<device type="pachinko" />
</peripherals>
<cartridge system="Famicom" dump="unknown" crc="C22C23AB" sha1="4CEA0ECDF0A22E678B827C9BFD8D80B5DEBB4094"> <cartridge system="Famicom" dump="unknown" crc="C22C23AB" sha1="4CEA0ECDF0A22E678B827C9BFD8D80B5DEBB4094">
<board mapper="1"> <board mapper="1">
<prg size="128k" /> <prg size="128k" />
@@ -20947,6 +21260,18 @@
</board> </board>
</cartridge> </cartridge>
</game> </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> <game>
<cartridge system="Dendy" dump="unknown" crc="C7EDBC2E" sha1="E4414C160C7E91136C62D99154336035E5636EEB"> <cartridge system="Dendy" dump="unknown" crc="C7EDBC2E" sha1="E4414C160C7E91136C62D99154336035E5636EEB">
<board mapper="13"> <board mapper="13">
@@ -22458,7 +22783,7 @@
</cartridge> </cartridge>
</game> </game>
<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"> <board type="NES-SNROM" mapper="1">
<prg size="128k" /> <prg size="128k" />
<vram size="8k" /> <vram size="8k" />
@@ -23635,6 +23960,9 @@
</cartridge> </cartridge>
</game> </game>
<game> <game>
<peripherals>
<device type="pachinko" />
</peripherals>
<cartridge system="Famicom" dump="unknown" crc="E08C8A60" sha1="BAF3A4E0423A86E53234E806843149EF7D0974A9"> <cartridge system="Famicom" dump="unknown" crc="E08C8A60" sha1="BAF3A4E0423A86E53234E806843149EF7D0974A9">
<board mapper="4"> <board mapper="4">
<prg size="512k" /> <prg size="512k" />
@@ -25207,7 +25535,7 @@
</game> </game>
<game> <game>
<cartridge system="Famicom" dump="unknown" crc="EEE9A682" sha1="46C443D0EB27AF7A566E744F096F981034A06E59"> <cartridge system="Famicom" dump="unknown" crc="EEE9A682" sha1="46C443D0EB27AF7A566E744F096F981034A06E59">
<board mapper="5"> <board type="HVC-ETROM" mapper="5">
<prg size="256k" /> <prg size="256k" />
<chr size="128k" /> <chr size="128k" />
<wram size="8k" /> <wram size="8k" />
@@ -25492,6 +25820,14 @@
</board> </board>
</cartridge> </cartridge>
</game> </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> <game>
<cartridge system="NES-NTSC" dump="ok" crc="F31D36A3" sha1="00147962462C44354735861D0258D72314635458"> <cartridge system="NES-NTSC" dump="ok" crc="F31D36A3" sha1="00147962462C44354735861D0258D72314635458">
<board type="NES-TSROM" mapper="4"> <board type="NES-TSROM" mapper="4">
@@ -25606,17 +25942,6 @@
</board> </board>
</cartridge> </cartridge>
</game> </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> <game>
<peripherals> <peripherals>
<device type="fourplayer" /> <device type="fourplayer" />
@@ -25698,7 +26023,7 @@
</game> </game>
<game> <game>
<cartridge system="Famicom" dump="unknown" crc="F540677B" sha1="44A5BC2B8156D50518EEBEEFA522A7642E0476DC"> <cartridge system="Famicom" dump="unknown" crc="F540677B" sha1="44A5BC2B8156D50518EEBEEFA522A7642E0476DC">
<board mapper="5"> <board type="HVC-EWROM" mapper="5">
<prg size="512k" /> <prg size="512k" />
<chr size="256k" /> <chr size="256k" />
<wram size="32k" battery="1" /> <wram size="32k" battery="1" />
@@ -26686,6 +27011,16 @@
</board> </board>
</cartridge> </cartridge>
</game> </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> <game>
<cartridge system="Famicom" dump="unknown" crc="FDD89C45" sha1="D25327E0F0D8539AE761DF861254BDE3A60FDD96"> <cartridge system="Famicom" dump="unknown" crc="FDD89C45" sha1="D25327E0F0D8539AE761DF861254BDE3A60FDD96">
<board mapper="0"> <board mapper="0">
@@ -26767,7 +27102,7 @@
</game> </game>
<game> <game>
<cartridge system="Famicom" dump="unknown" crc="FE3488D1" sha1="800AEFE756E85A0A78CCB4DAE68EBBA5DF24BF41"> <cartridge system="Famicom" dump="unknown" crc="FE3488D1" sha1="800AEFE756E85A0A78CCB4DAE68EBBA5DF24BF41">
<board mapper="5"> <board type="HVC-ETROM" mapper="5">
<prg size="512k" /> <prg size="512k" />
<chr size="128k" /> <chr size="128k" />
<wram size="8k" /> <wram size="8k" />
@@ -26916,4 +27251,15 @@
</board> </board>
</cartridge> </cartridge>
</game> </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> </database>
+12 -12
View File
@@ -1,7 +1,7 @@
{ {
"generated_at": "2026-03-27T22:52:09Z", "generated_at": "2026-03-28T08:10:17Z",
"total_files": 6756, "total_files": 6756,
"total_size": 5589782999, "total_size": 5589795834,
"files": { "files": {
"520d3d1b5897800af47f92efd2444a26b7a7dead": { "520d3d1b5897800af47f92efd2444a26b7a7dead": {
"path": "bios/3DO Company/3DO/3do_arcade_saot.bin", "path": "bios/3DO Company/3DO/3do_arcade_saot.bin",
@@ -33213,15 +33213,15 @@
"crc32": "5205222b", "crc32": "5205222b",
"adler32": "34389d20" "adler32": "34389d20"
}, },
"26322f182540211e9b5e3647675b7c593706ae2b": { "f92312bae56e29c5bf00a5103105fce78472bf5c": {
"path": "bios/Nintendo/NES/NstDatabase.xml", "path": "bios/Nintendo/NES/NstDatabase.xml",
"name": "NstDatabase.xml", "name": "NstDatabase.xml",
"size": 1009534, "size": 1022369,
"sha1": "26322f182540211e9b5e3647675b7c593706ae2b", "sha1": "f92312bae56e29c5bf00a5103105fce78472bf5c",
"md5": "7bfe8c0540ed4bd6a0f1e2a0f0118ced", "md5": "0ee6cbdc6f5c96ce9c8aa5edb59066f4",
"sha256": "914584ee6964e8b4cc4ff092874052e7baf13708cfb3f35940342421fcf1bedc", "sha256": "ef5bfd08928b4c9186ba03628ae9f2b7a3b0e30c9204592ac72cd67fa8a31f3a",
"crc32": "ebb2196c", "crc32": "0e4d552b",
"adler32": "88d01ea2" "adler32": "4f5c1356"
}, },
"f430a0d752a9fa0c7032db8131f9090d18f71779": { "f430a0d752a9fa0c7032db8131f9090d18f71779": {
"path": "bios/Nintendo/NES/gamegenie.nes", "path": "bios/Nintendo/NES/gamegenie.nes",
@@ -70887,7 +70887,7 @@
"b953eb1a8fc9922b3f7051c1cdc451f1": "ae7233cae8f94749796e0b740d6021e3b00a8926", "b953eb1a8fc9922b3f7051c1cdc451f1": "ae7233cae8f94749796e0b740d6021e3b00a8926",
"413154dd0e2c824c9b18b807fd03ec4e": "691e46213d8428befdf568157e670b971ab94e1d", "413154dd0e2c824c9b18b807fd03ec4e": "691e46213d8428befdf568157e670b971ab94e1d",
"c03f6bbaf644eb9b3ee261dbe199eb42": "2faaf92bcaffe675f54f7249d30f3791507e22ab", "c03f6bbaf644eb9b3ee261dbe199eb42": "2faaf92bcaffe675f54f7249d30f3791507e22ab",
"7bfe8c0540ed4bd6a0f1e2a0f0118ced": "26322f182540211e9b5e3647675b7c593706ae2b", "0ee6cbdc6f5c96ce9c8aa5edb59066f4": "f92312bae56e29c5bf00a5103105fce78472bf5c",
"7f98d77d7a094ad7d069b74bd553ec98": "f430a0d752a9fa0c7032db8131f9090d18f71779", "7f98d77d7a094ad7d069b74bd553ec98": "f430a0d752a9fa0c7032db8131f9090d18f71779",
"aaf3666e4ed478e2964b46d6a7aa27ad": "37027d92e1015b82a7dc5c43e9f1649a961577ab", "aaf3666e4ed478e2964b46d6a7aa27ad": "37027d92e1015b82a7dc5c43e9f1649a961577ab",
"8d3d9f294b6e174bc7b1d2fd1c727530": "bf861922dcb78c316360e3e742f4f70ff63c9bc3", "8d3d9f294b6e174bc7b1d2fd1c727530": "bf861922dcb78c316360e3e742f4f70ff63c9bc3",
@@ -83500,7 +83500,7 @@
"2faaf92bcaffe675f54f7249d30f3791507e22ab" "2faaf92bcaffe675f54f7249d30f3791507e22ab"
], ],
"NstDatabase.xml": [ "NstDatabase.xml": [
"26322f182540211e9b5e3647675b7c593706ae2b" "f92312bae56e29c5bf00a5103105fce78472bf5c"
], ],
"gamegenie.nes": [ "gamegenie.nes": [
"f430a0d752a9fa0c7032db8131f9090d18f71779" "f430a0d752a9fa0c7032db8131f9090d18f71779"
@@ -99007,7 +99007,7 @@
"54c7d10e": "ae7233cae8f94749796e0b740d6021e3b00a8926", "54c7d10e": "ae7233cae8f94749796e0b740d6021e3b00a8926",
"8bbef508": "691e46213d8428befdf568157e670b971ab94e1d", "8bbef508": "691e46213d8428befdf568157e670b971ab94e1d",
"5205222b": "2faaf92bcaffe675f54f7249d30f3791507e22ab", "5205222b": "2faaf92bcaffe675f54f7249d30f3791507e22ab",
"ebb2196c": "26322f182540211e9b5e3647675b7c593706ae2b", "0e4d552b": "f92312bae56e29c5bf00a5103105fce78472bf5c",
"4c514089": "f430a0d752a9fa0c7032db8131f9090d18f71779", "4c514089": "f430a0d752a9fa0c7032db8131f9090d18f71779",
"76f51d6b": "37027d92e1015b82a7dc5c43e9f1649a961577ab", "76f51d6b": "37027d92e1015b82a7dc5c43e9f1649a961577ab",
"7f933ce2": "bf861922dcb78c316360e3e742f4f70ff63c9bc3", "7f933ce2": "bf861922dcb78c316360e3e742f4f70ff63c9bc3",
+3 -3
View File
@@ -1325,9 +1325,9 @@ systems:
- name: NstDatabase.xml - name: NstDatabase.xml
destination: NstDatabase.xml destination: NstDatabase.xml
required: true required: true
sha1: 26322f182540211e9b5e3647675b7c593706ae2b sha1: f92312bae56e29c5bf00a5103105fce78472bf5c
md5: 7bfe8c0540ed4bd6a0f1e2a0f0118ced md5: 0ee6cbdc6f5c96ce9c8aa5edb59066f4
crc32: ebb2196c crc32: 0e4d552b
size: 1009534 size: 1009534
core: fceumm core: fceumm
manufacturer: Nintendo manufacturer: Nintendo
+1 -1
View File
@@ -7310,7 +7310,7 @@ systems:
- name: NstDatabase.xml - name: NstDatabase.xml
destination: bios/NstDatabase.xml destination: bios/NstDatabase.xml
required: false required: false
md5: 7bfe8c0540ed4bd6a0f1e2a0f0118ced md5: 0ee6cbdc6f5c96ce9c8aa5edb59066f4
nintendo-pokemon-mini: nintendo-pokemon-mini:
files: files:
- name: bios.min - name: bios.min
+2 -2
View File
@@ -960,8 +960,8 @@ systems:
- name: NstDatabase.xml - name: NstDatabase.xml
destination: nes/NstDatabase.xml destination: nes/NstDatabase.xml
required: true required: true
sha1: 26322f182540211e9b5e3647675b7c593706ae2b sha1: f92312bae56e29c5bf00a5103105fce78472bf5c
md5: 7bfe8c0540ed4bd6a0f1e2a0f0118ced md5: 0ee6cbdc6f5c96ce9c8aa5edb59066f4
crc32: ebb2196c crc32: ebb2196c
size: 1009534 size: 1009534
nintendo-pokemon-mini: nintendo-pokemon-mini:
+35 -5
View File
@@ -647,6 +647,29 @@ def resolve_platform_cores(
return result 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: def _norm_system_id(sid: str) -> str:
"""Normalize system ID for cross-platform matching. """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"). (e.g., "microsoft-xbox", "nintendo-wii-u").
""" """
s = sid.lower().replace("_", "-") s = sid.lower().replace("_", "-")
for prefix in ("microsoft-", "nintendo-", "sony-", "sega-", for prefix in MANUFACTURER_PREFIXES:
"snk-", "panasonic-", "nec-", "epoch-", "mattel-",
"fairchild-", "hartung-", "tiger-", "magnavox-",
"philips-", "bandai-", "casio-", "coleco-",
"commodore-", "sharp-", "sinclair-"):
if s.startswith(prefix): if s.startswith(prefix):
s = s[len(prefix):] s = s[len(prefix):]
break break
@@ -1068,3 +1087,14 @@ def list_system_ids(emulators_dir: str) -> None:
for sys_id in sorted(system_emus): for sys_id in sorted(system_emus):
count = len(system_emus[sys_id]) count = len(system_emus[sys_id])
print(f" {sys_id:35s} ({count} emulator{'s' if count > 1 else ''})") 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}")
+519 -32
View File
@@ -16,6 +16,7 @@ import argparse
import hashlib import hashlib
import json import json
import os import os
import re
import sys import sys
import tempfile import tempfile
import urllib.request import urllib.request
@@ -25,9 +26,11 @@ from pathlib import Path
sys.path.insert(0, os.path.dirname(__file__)) sys.path.insert(0, os.path.dirname(__file__))
from common import ( from common import (
MANUFACTURER_PREFIXES,
_build_validation_index, build_zip_contents_index, check_file_validation, _build_validation_index, build_zip_contents_index, check_file_validation,
check_inside_zip, compute_hashes, fetch_large_file, filter_files_by_mode, 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, filter_systems_by_target, list_system_ids, load_database,
load_data_dir_registry, load_emulator_profiles, load_platform_config, load_data_dir_registry, load_emulator_profiles, load_platform_config,
md5_composite, resolve_local_file, md5_composite, resolve_local_file,
@@ -46,6 +49,106 @@ DEFAULT_OUTPUT_DIR = "dist"
DEFAULT_BIOS_DIR = "bios" DEFAULT_BIOS_DIR = "bios"
MAX_ENTRY_SIZE = 512 * 1024 * 1024 # 512MB 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( def _find_candidate_satisfying_both(
file_entry: dict, file_entry: dict,
@@ -231,6 +334,9 @@ def generate_pack(
data_registry: dict | None = None, data_registry: dict | None = None,
emu_profiles: dict | None = None, emu_profiles: dict | None = None,
target_cores: set[str] | 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: ) -> str | None:
"""Generate a ZIP pack for a platform. """Generate a ZIP pack for a platform.
@@ -246,7 +352,22 @@ def generate_pack(
version = config.get("version", config.get("dat_version", "")) version = config.get("version", config.get("dat_version", ""))
version_tag = f"_{version.replace(' ', '')}" if version else "" 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) zip_path = os.path.join(output_dir, zip_name)
os.makedirs(output_dir, exist_ok=True) os.makedirs(output_dir, exist_ok=True)
@@ -279,9 +400,23 @@ def generate_pack(
platform_cores=plat_cores, 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: with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
for sys_id, system in sorted(pack_systems.items()): for sys_id, system in sorted(pack_systems.items()):
for file_entry in system.get("files", []): 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"])) dest = _sanitize_path(file_entry.get("destination", file_entry["name"]))
if not dest: if not dest:
# EmuDeck-style entries (system:md5 whitelist, no filename). # 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 # Core requirements: files platform's cores need but YAML doesn't declare
if emu_profiles is None: if emu_profiles is None:
emu_profiles = load_emulator_profiles(emulators_dir) emu_profiles = load_emulator_profiles(emulators_dir)
core_files = _collect_emulator_extras( if precomputed_extras is not None:
config, emulators_dir, db, core_files = precomputed_extras
seen_destinations, base_dest, emu_profiles, target_cores=target_cores, 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 core_count = 0
for fe in core_files: for fe in core_files:
if required_only and fe.get("required") is False:
continue
dest = _sanitize_path(fe.get("destination", fe["name"])) dest = _sanitize_path(fe.get("destination", fe["name"]))
if not dest: if not dest:
continue continue
@@ -591,6 +733,7 @@ def generate_emulator_pack(
output_dir: str, output_dir: str,
standalone: bool = False, standalone: bool = False,
zip_contents: dict | None = None, zip_contents: dict | None = None,
required_only: bool = False,
) -> str | None: ) -> str | None:
"""Generate a ZIP pack for specific emulator profiles.""" """Generate a ZIP pack for specific emulator profiles."""
all_profiles = load_emulator_profiles(emulators_dir, skip_aliases=False) all_profiles = load_emulator_profiles(emulators_dir, skip_aliases=False)
@@ -710,6 +853,8 @@ def generate_emulator_pack(
# Pack individual files (skip archived ones) # Pack individual files (skip archived ones)
for fe in files: for fe in files:
if required_only and fe.get("required") is False:
continue
if fe.get("archive"): if fe.get("archive"):
continue continue
@@ -801,6 +946,7 @@ def generate_system_pack(
output_dir: str, output_dir: str,
standalone: bool = False, standalone: bool = False,
zip_contents: dict | None = None, zip_contents: dict | None = None,
required_only: bool = False,
) -> str | None: ) -> str | None:
"""Generate a ZIP pack for all emulators supporting given system IDs.""" """Generate a ZIP pack for all emulators supporting given system IDs."""
profiles = load_emulator_profiles(emulators_dir) profiles = load_emulator_profiles(emulators_dir)
@@ -835,7 +981,7 @@ def generate_system_pack(
) )
result = generate_emulator_pack( result = generate_emulator_pack(
matching, emulators_dir, db, bios_dir, output_dir, matching, emulators_dir, db, bios_dir, output_dir,
standalone, zip_contents, standalone, zip_contents, required_only=required_only,
) )
if result: if result:
# Rename to system-based name # 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) 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(): def main():
parser = argparse.ArgumentParser(description="Generate platform BIOS ZIP packs") parser = argparse.ArgumentParser(description="Generate platform BIOS ZIP packs")
parser.add_argument("--platform", "-p", help="Platform name (e.g., retroarch)") 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", parser.add_argument("--refresh-data", action="store_true",
help="Force re-download all data directories") help="Force re-download all data directories")
parser.add_argument("--list", action="store_true", help="List available platforms") 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("--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("--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() args = parser.parse_args()
if args.list: if args.list:
@@ -887,7 +1291,10 @@ def main():
list_emulator_profiles(args.emulators_dir) list_emulator_profiles(args.emulators_dir)
return return
if args.list_systems: 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 return
if args.list_targets: if args.list_targets:
if not args.platform: if not args.platform:
@@ -902,18 +1309,68 @@ def main():
print(f" {t['name']:30s} {t['architecture']:10s} {t['core_count']:>4d} cores{aliases}") print(f" {t['name']:30s} {t['architecture']:10s} {t['core_count']:>4d} cores{aliases}")
return return
# Mutual exclusion # Mode validation
modes = sum(1 for x in (args.platform, args.all, args.emulator, args.system) if x) has_platform = bool(args.platform)
if modes == 0: has_all = args.all
parser.error("Specify --platform, --all, --emulator, or --system") has_emulator = bool(args.emulator)
if modes > 1: has_system = bool(args.system)
parser.error("--platform, --all, --emulator, and --system are mutually exclusive") has_from_md5 = bool(args.from_md5 or getattr(args, 'from_md5_file', None))
if args.standalone and not (args.emulator or args.system):
parser.error("--standalone requires --emulator or --system") if args.from_md5 and getattr(args, 'from_md5_file', None):
if args.target and not (args.platform or args.all): 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") parser.error("--target requires --platform or --all")
if args.target and (args.emulator or args.system): if args.target and has_emulator:
parser.error("--target is incompatible with --emulator and --system") 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) db = load_database(args.db)
zip_contents = build_zip_contents_index(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()] names = [n.strip() for n in args.emulator.split(",") if n.strip()]
result = generate_emulator_pack( result = generate_emulator_pack(
names, args.emulators_dir, db, args.bios_dir, args.output_dir, 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: if not result:
sys.exit(1) sys.exit(1)
return return
# System mode # System mode (standalone, without platform context)
if args.system: if has_system and not has_platform and not has_all:
system_ids = [s.strip() for s in args.system.split(",") if s.strip()] system_ids = [s.strip() for s in args.system.split(",") if s.strip()]
result = generate_system_pack( result = generate_system_pack(
system_ids, args.emulators_dir, db, args.bios_dir, args.output_dir, 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: if not result:
sys.exit(1) sys.exit(1)
return return
system_filter = None
if args.system:
system_filter = [s.strip() for s in args.system.split(",") if s.strip()]
# Platform mode (existing) # Platform mode (existing)
if args.all: if args.all:
platforms = list_registered_platforms( platforms = list_registered_platforms(
@@ -998,13 +1459,25 @@ def main():
try: try:
tc = target_cores_cache.get(representative) if args.target else None tc = target_cores_cache.get(representative) if args.target else None
zip_path = generate_pack( if args.split:
representative, args.platforms_dir, db, args.bios_dir, args.output_dir, zip_paths = generate_split_packs(
include_extras=args.include_extras, emulators_dir=args.emulators_dir, representative, args.platforms_dir, db, args.bios_dir,
zip_contents=zip_contents, data_registry=data_registry, args.output_dir, group_by=args.group_by,
emu_profiles=emu_profiles, target_cores=tc, emulators_dir=args.emulators_dir, zip_contents=zip_contents,
) data_registry=data_registry, emu_profiles=emu_profiles,
if zip_path and variants: 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) rep_cfg = load_platform_config(representative, args.platforms_dir)
ver = rep_cfg.get("version", rep_cfg.get("dat_version", "")) ver = rep_cfg.get("version", rep_cfg.get("dat_version", ""))
ver_tag = f"_{ver.replace(' ', '')}" if ver else "" ver_tag = f"_{ver.replace(' ', '')}" if ver else ""
@@ -1020,7 +1493,17 @@ def main():
# Post-generation: verify all packs + inject manifests + SHA256SUMS # Post-generation: verify all packs + inject manifests + SHA256SUMS
if not args.list_emulators and not args.list_systems: if not args.list_emulators and not args.list_systems:
print("\nVerifying packs and generating manifests...") 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: if not all_ok:
print("WARNING: some packs have verification errors") print("WARNING: some packs have verification errors")
sys.exit(1) sys.exit(1)
@@ -1267,7 +1750,8 @@ def verify_pack_against_platform(
def verify_and_finalize_packs(output_dir: str, db: dict, 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. """Verify all packs, inject manifests, generate SHA256SUMS.
Two-stage verification: Two-stage verification:
@@ -1307,6 +1791,9 @@ def verify_and_finalize_packs(output_dir: str, db: dict,
inject_manifest(zip_path, manifest) inject_manifest(zip_path, manifest)
# Stage 2: platform conformance (extract + verify) # Stage 2: platform conformance (extract + verify)
# Skipped for filtered/split/custom packs (intentionally partial)
if skip_conformance:
continue
platforms = pack_to_platform.get(name, []) platforms = pack_to_platform.get(name, [])
for pname in platforms: for pname in platforms:
(p_ok, total, matched, p_errors, (p_ok, total, matched, p_errors,
+21
View File
@@ -1604,10 +1604,22 @@ def generate_wiki_tools() -> str:
"Build platform-specific BIOS ZIP packs.", "Build platform-specific BIOS ZIP packs.",
"", "",
"```bash", "```bash",
"# Full platform packs",
"python scripts/generate_pack.py --all --output-dir dist/", "python scripts/generate_pack.py --all --output-dir dist/",
"python scripts/generate_pack.py --platform batocera", "python scripts/generate_pack.py --platform batocera",
"python scripts/generate_pack.py --emulator dolphin", "python scripts/generate_pack.py --emulator dolphin",
"python scripts/generate_pack.py --system atari-lynx", "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.", "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.", "the tool searches for a variant that satisfies both.",
"If none exists, the platform version is kept and the discrepancy is reported.", "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", "### cross_reference.py",
"", "",
"Compare emulator profiles against platform configs.", "Compare emulator profiles against platform configs.",
+516
View File
@@ -1539,6 +1539,522 @@ class TestE2E(unittest.TestCase):
self.assertEqual(gt["total"], result["total_files"]) self.assertEqual(gt["total"], result["total_files"])
self.assertGreaterEqual(gt["with_validation"], 1) 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__": if __name__ == "__main__":
unittest.main() unittest.main()
+21
View File
@@ -51,10 +51,22 @@ Verification modes per platform:
Build platform-specific BIOS ZIP packs. Build platform-specific BIOS ZIP packs.
```bash ```bash
# Full platform packs
python scripts/generate_pack.py --all --output-dir dist/ python scripts/generate_pack.py --all --output-dir dist/
python scripts/generate_pack.py --platform batocera python scripts/generate_pack.py --platform batocera
python scripts/generate_pack.py --emulator dolphin python scripts/generate_pack.py --emulator dolphin
python scripts/generate_pack.py --system atari-lynx 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. 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. the tool searches for a variant that satisfies both.
If none exists, the platform version is kept and the discrepancy is reported. 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 ### cross_reference.py
Compare emulator profiles against platform configs. Compare emulator profiles against platform configs.