diff --git a/install/batocera.json b/install/batocera.json index 7e36f86c..e06b947c 100644 --- a/install/batocera.json +++ b/install/batocera.json @@ -241,10 +241,10 @@ } } ], - "total_files": 12899, - "total_size": 9279360834, - "pack_files": 12901, - "pack_size": 9279360834, + "total_files": 12907, + "total_size": 9288027834, + "pack_files": 12909, + "pack_size": 9288027834, "total_omitted": 395, "omitted_files": [ { @@ -128447,6 +128447,62 @@ "storage": "release", "release_asset": "demo00.pk4" }, + { + "dest": "dolphin-emu/Sys/GC/dsp_rom.bin", + "sha1": "3c6cc6e04fdd0b2a392d7a6ed769455444846be7", + "sha256": "49d987ee1eab29a157425b82d54516957a81e1bac247c8834e494642605c3e8c", + "size": 8192, + "repo_path": "bios/Nintendo/GameCube/.variants/dsp_rom.bin.3c6cc6e0", + "cores": [ + "dolphin" + ], + "systems": [ + "nintendo-gamecube", + "triforce" + ] + }, + { + "dest": "dolphin-emu/Sys/GC/dsp_coef.bin", + "sha1": "c116d867ba001dcd6bf6d399ff4bf38d340f556c", + "sha256": "a1e269cfdc6b82d5a55baab5a0bbbb3b9f953ad92729e0d5ec50dbc2746a2c47", + "size": 4096, + "repo_path": "bios/Nintendo/GameCube/.variants/dsp_coef.bin.c116d867", + "cores": [ + "dolphin" + ], + "systems": [ + "nintendo-gamecube", + "triforce" + ] + }, + { + "dest": "dolphin-emu/Sys/GC/font_western.bin", + "sha1": "483a17ff25b68a8f18cae691da99f16beefbe370", + "sha256": "4ad991be2b0aa305f09b90a79fc50f57833b30b008890b5ef1336cc3d9d0bae0", + "size": 6478, + "repo_path": "bios/Other/dolphin-mmjr/font_western.bin", + "cores": [ + "dolphin" + ], + "systems": [ + "nintendo-gamecube", + "triforce" + ] + }, + { + "dest": "dolphin-emu/Sys/GC/font_japanese.bin", + "sha1": "7fbbedcc7a1ae00414a4e9e967109cc78be1fe3b", + "sha256": "38f9f59d505bc4e2d86b0196706195f33ad72b7fe9d029cf263072cde19d044f", + "size": 259626, + "repo_path": "bios/Other/dolphin-mmjr/font_japanese.bin", + "cores": [ + "dolphin" + ], + "systems": [ + "nintendo-gamecube", + "triforce" + ] + }, { "dest": "GC/dsp_rom.bin", "sha1": "3c6cc6e04fdd0b2a392d7a6ed769455444846be7", @@ -140832,6 +140888,80 @@ "mame" ] }, + { + "dest": "dolphin-emu/Sys/GC/USA/IPL.bin", + "sha1": "ef9194ab4804aa0aa8540d846caf291b28331165", + "sha256": "7c833f95839c226699108b93ab6acbe772788bfa3f05132b8ced5e78f5bc9a26", + "size": 2097152, + "repo_path": "bios/Nintendo/GameCube/GC/USA/IPL.bin", + "cores": [ + "dolphin" + ], + "systems": [ + "nintendo-gamecube", + "triforce" + ], + "regions": [ + "north-america" + ], + "region_groups": [ + "Dolphin:file:dolphin-emu/Sys/GC/USA/IPL.bin" + ] + }, + { + "dest": "dolphin-emu/Sys/GC/EUR/IPL.bin", + "sha1": "80b8744ff5e43585392f55546bd03a673d11ef5f", + "sha256": "85ffcbc834238eb24c87fce27ba7b438cc53a77db013a0a7c59c78e0af08cb84", + "size": 2097152, + "repo_path": "bios/Nintendo/GameCube/GC/EUR/IPL.bin", + "cores": [ + "dolphin" + ], + "systems": [ + "nintendo-gamecube", + "triforce" + ], + "regions": [ + "europe", + "oceania" + ], + "region_groups": [ + "Dolphin:file:dolphin-emu/Sys/GC/EUR/IPL.bin" + ] + }, + { + "dest": "dolphin-emu/Sys/GC/JAP/IPL.bin", + "sha1": "a1837968288253ed541f2b11440b68f5a9b33875", + "sha256": "5b2f2617cc63f1861a1f4ae44bb135caf10e202b5b9b79271daaf60f8caaf37d", + "size": 2097152, + "repo_path": "bios/Nintendo/GameCube/GC/JAP/IPL.bin", + "cores": [ + "dolphin" + ], + "systems": [ + "nintendo-gamecube", + "triforce" + ], + "regions": [ + "japan" + ], + "region_groups": [ + "Dolphin:file:dolphin-emu/Sys/GC/JAP/IPL.bin" + ] + }, + { + "dest": "dolphin-emu/Sys/Triforce/segaboot.gcm", + "sha1": "7297a11be143011ceb75a0720f96855003415a5d", + "sha256": "9a2eee93f5ed961aea4b12a318e2b4a54fa69a31239c1378b8bef6ea02e90391", + "size": 2097152, + "repo_path": "bios/Sega/Triforce/segaboot.gcm", + "cores": [ + "dolphin" + ], + "systems": [ + "triforce" + ] + }, { "dest": "Triforce/segaboot.gcm", "sha1": "7297a11be143011ceb75a0720f96855003415a5d", diff --git a/install/recalbox.json b/install/recalbox.json index 1f785ca5..7330ff0d 100644 --- a/install/recalbox.json +++ b/install/recalbox.json @@ -239,10 +239,10 @@ } } ], - "total_files": 3418, - "total_size": 4183759111, - "pack_files": 3420, - "pack_size": 4183759111, + "total_files": 3426, + "total_size": 4192426111, + "pack_files": 3428, + "pack_size": 4192426111, "total_omitted": 9, "omitted_files": [ { @@ -33254,6 +33254,68 @@ "corsixth" ] }, + { + "dest": "dolphin-emu/Sys/GC/dsp_rom.bin", + "sha1": "3c6cc6e04fdd0b2a392d7a6ed769455444846be7", + "sha256": "49d987ee1eab29a157425b82d54516957a81e1bac247c8834e494642605c3e8c", + "size": 8192, + "repo_path": "bios/Nintendo/GameCube/.variants/dsp_rom.bin.3c6cc6e0", + "cores": [ + "dolphin" + ], + "systems": [ + "nintendo-gamecube" + ] + }, + { + "dest": "dolphin-emu/Sys/GC/dsp_coef.bin", + "sha1": "c116d867ba001dcd6bf6d399ff4bf38d340f556c", + "sha256": "a1e269cfdc6b82d5a55baab5a0bbbb3b9f953ad92729e0d5ec50dbc2746a2c47", + "size": 4096, + "repo_path": "bios/Nintendo/GameCube/.variants/dsp_coef.bin.c116d867", + "cores": [ + "dolphin" + ], + "systems": [ + "nintendo-gamecube" + ] + }, + { + "dest": "dolphin-emu/Sys/GC/font_western.bin", + "sha1": "483a17ff25b68a8f18cae691da99f16beefbe370", + "sha256": "4ad991be2b0aa305f09b90a79fc50f57833b30b008890b5ef1336cc3d9d0bae0", + "size": 6478, + "repo_path": "bios/Other/dolphin-mmjr/font_western.bin", + "cores": [ + "dolphin" + ], + "systems": [ + "nintendo-gamecube" + ] + }, + { + "dest": "dolphin-emu/Sys/GC/font_japanese.bin", + "sha1": "7fbbedcc7a1ae00414a4e9e967109cc78be1fe3b", + "sha256": "38f9f59d505bc4e2d86b0196706195f33ad72b7fe9d029cf263072cde19d044f", + "size": 259626, + "repo_path": "bios/Other/dolphin-mmjr/font_japanese.bin", + "cores": [ + "dolphin" + ], + "systems": [ + "nintendo-gamecube" + ] + }, + { + "dest": "dolphin-emu/Sys/Triforce/segaboot.gcm", + "sha1": "7297a11be143011ceb75a0720f96855003415a5d", + "sha256": "9a2eee93f5ed961aea4b12a318e2b4a54fa69a31239c1378b8bef6ea02e90391", + "size": 2097152, + "repo_path": "bios/Sega/Triforce/segaboot.gcm", + "cores": [ + "dolphin" + ] + }, { "dest": "GC/dsp_rom.bin", "sha1": "3c6cc6e04fdd0b2a392d7a6ed769455444846be7", @@ -38601,6 +38663,64 @@ "nintendo-ds" ] }, + { + "dest": "dolphin-emu/Sys/GC/USA/IPL.bin", + "sha1": "ef9194ab4804aa0aa8540d846caf291b28331165", + "sha256": "7c833f95839c226699108b93ab6acbe772788bfa3f05132b8ced5e78f5bc9a26", + "size": 2097152, + "repo_path": "bios/Nintendo/GameCube/GC/USA/IPL.bin", + "cores": [ + "dolphin" + ], + "systems": [ + "nintendo-gamecube" + ], + "regions": [ + "north-america" + ], + "region_groups": [ + "Dolphin:file:dolphin-emu/Sys/GC/USA/IPL.bin" + ] + }, + { + "dest": "dolphin-emu/Sys/GC/EUR/IPL.bin", + "sha1": "80b8744ff5e43585392f55546bd03a673d11ef5f", + "sha256": "85ffcbc834238eb24c87fce27ba7b438cc53a77db013a0a7c59c78e0af08cb84", + "size": 2097152, + "repo_path": "bios/Nintendo/GameCube/GC/EUR/IPL.bin", + "cores": [ + "dolphin" + ], + "systems": [ + "nintendo-gamecube" + ], + "regions": [ + "europe", + "oceania" + ], + "region_groups": [ + "Dolphin:file:dolphin-emu/Sys/GC/EUR/IPL.bin" + ] + }, + { + "dest": "dolphin-emu/Sys/GC/JAP/IPL.bin", + "sha1": "a1837968288253ed541f2b11440b68f5a9b33875", + "sha256": "5b2f2617cc63f1861a1f4ae44bb135caf10e202b5b9b79271daaf60f8caaf37d", + "size": 2097152, + "repo_path": "bios/Nintendo/GameCube/GC/JAP/IPL.bin", + "cores": [ + "dolphin" + ], + "systems": [ + "nintendo-gamecube" + ], + "regions": [ + "japan" + ], + "region_groups": [ + "Dolphin:file:dolphin-emu/Sys/GC/JAP/IPL.bin" + ] + }, { "dest": "GC/USA/IPL.bin", "sha1": "ef9194ab4804aa0aa8540d846caf291b28331165", diff --git a/install/retrobat.json b/install/retrobat.json index ee346913..ebf8a3b4 100644 --- a/install/retrobat.json +++ b/install/retrobat.json @@ -245,10 +245,10 @@ } } ], - "total_files": 7740, - "total_size": 7988832663, - "pack_files": 7742, - "pack_size": 7988832663, + "total_files": 7751, + "total_size": 7997666672, + "pack_files": 7753, + "pack_size": 7997666672, "total_omitted": 135, "omitted_files": [ { @@ -70316,6 +70316,36 @@ } ] }, + { + "dest": "supermodel/Games.xml", + "sha1": "fa334ab5ba6366bd28168860c913e6365b9fec96", + "sha256": "275832cdb077c22883192f5aaeb8f76547f4dce786370f10b2d54c46a8ed3c5e", + "size": 148364, + "repo_path": "bios/Sega/Model 3/supermodel/Games.xml", + "cores": [ + "supermodel" + ] + }, + { + "dest": "supermodel/Supermodel.ini", + "sha1": "cbda2b3fa65e37c5c2824a4630f95f145533c360", + "sha256": "3c910e7aa207ea55d1946fedaf12718115c21be5e67abf35b1220080c6c67919", + "size": 9421, + "repo_path": "bios/Sega/Model 3/supermodel/Supermodel.ini", + "cores": [ + "supermodel" + ] + }, + { + "dest": "supermodel/Music.xml", + "sha1": "b44ed959fe2948a03d81b98e67ca32f2b4c37e78", + "sha256": "cf7555ca86e8640ee5bbb8e0c0897b073fc86647a93eda10b05065227610478c", + "size": 9224, + "repo_path": "bios/Sega/Model 3/Config/Music.xml", + "cores": [ + "supermodel" + ] + }, { "dest": "Config/Games.xml", "sha1": "6871a47de90ca67f9ff8e4e280416851a8164563", @@ -74985,6 +75015,116 @@ "storage": "release", "release_asset": "demo00.pk4" }, + { + "dest": "dolphin-emu/Sys/GC/USA/IPL.bin", + "sha1": "ef9194ab4804aa0aa8540d846caf291b28331165", + "sha256": "7c833f95839c226699108b93ab6acbe772788bfa3f05132b8ced5e78f5bc9a26", + "size": 2097152, + "repo_path": "bios/Nintendo/GameCube/GC/USA/IPL.bin", + "cores": [ + "dolphin" + ], + "systems": [ + "triforce" + ], + "regions": [ + "north-america" + ], + "region_groups": [ + "Dolphin:file:dolphin-emu/Sys/GC/USA/IPL.bin" + ] + }, + { + "dest": "dolphin-emu/Sys/GC/EUR/IPL.bin", + "sha1": "80b8744ff5e43585392f55546bd03a673d11ef5f", + "sha256": "85ffcbc834238eb24c87fce27ba7b438cc53a77db013a0a7c59c78e0af08cb84", + "size": 2097152, + "repo_path": "bios/Nintendo/GameCube/GC/EUR/IPL.bin", + "cores": [ + "dolphin" + ], + "systems": [ + "triforce" + ], + "regions": [ + "europe", + "oceania" + ], + "region_groups": [ + "Dolphin:file:dolphin-emu/Sys/GC/EUR/IPL.bin" + ] + }, + { + "dest": "dolphin-emu/Sys/GC/JAP/IPL.bin", + "sha1": "a1837968288253ed541f2b11440b68f5a9b33875", + "sha256": "5b2f2617cc63f1861a1f4ae44bb135caf10e202b5b9b79271daaf60f8caaf37d", + "size": 2097152, + "repo_path": "bios/Nintendo/GameCube/GC/JAP/IPL.bin", + "cores": [ + "dolphin" + ], + "systems": [ + "triforce" + ], + "regions": [ + "japan" + ], + "region_groups": [ + "Dolphin:file:dolphin-emu/Sys/GC/JAP/IPL.bin" + ] + }, + { + "dest": "dolphin-emu/Sys/GC/dsp_rom.bin", + "sha1": "3c6cc6e04fdd0b2a392d7a6ed769455444846be7", + "sha256": "49d987ee1eab29a157425b82d54516957a81e1bac247c8834e494642605c3e8c", + "size": 8192, + "repo_path": "bios/Nintendo/GameCube/.variants/dsp_rom.bin.3c6cc6e0", + "cores": [ + "dolphin" + ], + "systems": [ + "triforce" + ] + }, + { + "dest": "dolphin-emu/Sys/GC/dsp_coef.bin", + "sha1": "c116d867ba001dcd6bf6d399ff4bf38d340f556c", + "sha256": "a1e269cfdc6b82d5a55baab5a0bbbb3b9f953ad92729e0d5ec50dbc2746a2c47", + "size": 4096, + "repo_path": "bios/Nintendo/GameCube/.variants/dsp_coef.bin.c116d867", + "cores": [ + "dolphin" + ], + "systems": [ + "triforce" + ] + }, + { + "dest": "dolphin-emu/Sys/GC/font_western.bin", + "sha1": "483a17ff25b68a8f18cae691da99f16beefbe370", + "sha256": "4ad991be2b0aa305f09b90a79fc50f57833b30b008890b5ef1336cc3d9d0bae0", + "size": 6478, + "repo_path": "bios/Other/dolphin-mmjr/font_western.bin", + "cores": [ + "dolphin" + ], + "systems": [ + "triforce" + ] + }, + { + "dest": "dolphin-emu/Sys/GC/font_japanese.bin", + "sha1": "7fbbedcc7a1ae00414a4e9e967109cc78be1fe3b", + "sha256": "38f9f59d505bc4e2d86b0196706195f33ad72b7fe9d029cf263072cde19d044f", + "size": 259626, + "repo_path": "bios/Other/dolphin-mmjr/font_japanese.bin", + "cores": [ + "dolphin" + ], + "systems": [ + "triforce" + ] + }, { "dest": "GC/USA/IPL.bin", "sha1": "ef9194ab4804aa0aa8540d846caf291b28331165", @@ -87723,6 +87863,19 @@ "mame2016" ] }, + { + "dest": "dolphin-emu/Sys/Triforce/segaboot.gcm", + "sha1": "7297a11be143011ceb75a0720f96855003415a5d", + "sha256": "9a2eee93f5ed961aea4b12a318e2b4a54fa69a31239c1378b8bef6ea02e90391", + "size": 2097152, + "repo_path": "bios/Sega/Triforce/segaboot.gcm", + "cores": [ + "dolphin" + ], + "systems": [ + "triforce" + ] + }, { "dest": "GBA/gba_bios.bin", "sha1": "300c20df6731a33952ded8c436f7f186d25d3492", diff --git a/install/rocknix.json b/install/rocknix.json index f10f6111..6dfbd265 100644 --- a/install/rocknix.json +++ b/install/rocknix.json @@ -17,10 +17,10 @@ } ], "standalone_copies": [], - "total_files": 6115, - "total_size": 7848090128, - "pack_files": 6117, - "pack_size": 7848090128, + "total_files": 6132, + "total_size": 7913710008, + "pack_files": 6134, + "pack_size": 7913710008, "total_omitted": 49, "omitted_files": [ { @@ -1964,6 +1964,7 @@ "size": 4194304, "repo_path": "bios/Sony/PlayStation 2/ps2-0230a-20080220.bin", "cores": [ + "armsx2", "yaps2" ] } @@ -2879,6 +2880,46 @@ "cannonball" ] }, + { + "dest": "Cemu/resources/sharedFonts/CafeCn.ttf", + "sha1": "2d95227b9b3eaf1c74d9293ecd0d190ec7ffbbe3", + "sha256": "6afd751a9bb76bc788efb50a138c6f3765624ee84d8d8203be04d871ade40a6a", + "size": 9664448, + "repo_path": "bios/Nintendo/Wii U/resources/sharedFonts/CafeCn.ttf", + "cores": [ + "cemu" + ] + }, + { + "dest": "Cemu/resources/sharedFonts/CafeKr.ttf", + "sha1": "f0ef38fcc6133c24f4f38ff9b5c06b37e970d3cc", + "sha256": "c3c326f6ce1899e391dc4996b10a0c30558c5b8f36dfbaa418976862b3e2dffb", + "size": 5626464, + "repo_path": "bios/Nintendo/Wii U/resources/sharedFonts/CafeKr.ttf", + "cores": [ + "cemu" + ] + }, + { + "dest": "Cemu/resources/sharedFonts/CafeStd.ttf", + "sha1": "da0d4bcc3b13339179bee946eba16adfd731b4d3", + "sha256": "59e2494727b8e7a7bbf48c8749f2dbc07d438c9fabeaa9847a08ec690f9888a1", + "size": 5363768, + "repo_path": "bios/Nintendo/Wii U/resources/sharedFonts/CafeStd.ttf", + "cores": [ + "cemu" + ] + }, + { + "dest": "Cemu/resources/sharedFonts/CafeTw.ttf", + "sha1": "8ec796192ebc710f23ab6bd55453cae523b70444", + "sha256": "c8f9e16345dbc3b6e165fe12435486a748e730ebe6002b5d713efe73d171d51c", + "size": 6593548, + "repo_path": "bios/Nintendo/Wii U/resources/sharedFonts/CafeTw.ttf", + "cores": [ + "cemu" + ] + }, { "dest": "resources/sharedFonts/CafeCn.ttf", "sha1": "2d95227b9b3eaf1c74d9293ecd0d190ec7ffbbe3", @@ -47546,6 +47587,36 @@ "spmp8000emu" ] }, + { + "dest": "supermodel/Games.xml", + "sha1": "fa334ab5ba6366bd28168860c913e6365b9fec96", + "sha256": "275832cdb077c22883192f5aaeb8f76547f4dce786370f10b2d54c46a8ed3c5e", + "size": 148364, + "repo_path": "bios/Sega/Model 3/supermodel/Games.xml", + "cores": [ + "supermodel" + ] + }, + { + "dest": "supermodel/Supermodel.ini", + "sha1": "cbda2b3fa65e37c5c2824a4630f95f145533c360", + "sha256": "3c910e7aa207ea55d1946fedaf12718115c21be5e67abf35b1220080c6c67919", + "size": 9421, + "repo_path": "bios/Sega/Model 3/supermodel/Supermodel.ini", + "cores": [ + "supermodel" + ] + }, + { + "dest": "supermodel/Music.xml", + "sha1": "b44ed959fe2948a03d81b98e67ca32f2b4c37e78", + "sha256": "cf7555ca86e8640ee5bbb8e0c0897b073fc86647a93eda10b05065227610478c", + "size": 9224, + "repo_path": "bios/Sega/Model 3/Config/Music.xml", + "cores": [ + "supermodel" + ] + }, { "dest": "Config/Games.xml", "sha1": "6871a47de90ca67f9ff8e4e280416851a8164563", @@ -50323,6 +50394,90 @@ "cdi2015" ] }, + { + "dest": "Cemu/keys.txt", + "sha1": "9edc52be45201ec99f6e4ceb5dc6abfc633c4eae", + "sha256": "c5b9ee2a14cf6a0effb3b2f0f4b93be75823da80ebf5f0ca6a0f2be8b5aa5bfd", + "size": 449507, + "repo_path": "bios/Nintendo/Wii U/keys.txt", + "cores": [ + "cemu" + ] + }, + { + "dest": "Cemu/otp.bin", + "sha1": "aa4ebc7f94c22b04945b6c9232547ada10b71f6f", + "sha256": "a12993f7fe069098ab6fe1cdb43099a1dfb568223bef08ed5cb43ec93d9ff2de", + "size": 1024, + "repo_path": "bios/Nintendo/Wii U/otp.bin", + "cores": [ + "cemu" + ] + }, + { + "dest": "Cemu/seeprom.bin", + "sha1": "d08ffcf19945b63c7baf35d9965a9cd6bf61332b", + "sha256": "eab425e6e65430232ec87a776d352f8fe1be4905df3a6bdaa0941c03e20069bc", + "size": 512, + "repo_path": "bios/Nintendo/Wii U/seeprom.bin", + "cores": [ + "cemu" + ] + }, + { + "dest": "Cemu/shareddata.bin", + "sha1": "cd34a0ee9b7a8cbb9c9eae34739704a9dd4dd413", + "sha256": "662bc6c693c88040531fbe51186ce203e188d1c67ad788b2013b3c91ce595420", + "size": 33554432, + "repo_path": "bios/Nintendo/Wii U/shareddata.bin", + "cores": [ + "cemu" + ] + }, + { + "dest": "Cemu/cafeLibs/snd_user.rpl", + "sha1": "4e0ff91f1881203e404cd41ec34725b43eec5cf2", + "sha256": "45b0dbe8926a263d86bc4e77084815e58336b9b437196d48bc3da3c5b2b2675e", + "size": 84032, + "repo_path": "bios/Nintendo/Wii U/cafeLibs/snd_user.rpl", + "cores": [ + "cemu" + ], + "executable": true + }, + { + "dest": "Cemu/cafeLibs/snduser2.rpl", + "sha1": "38eac5a4695e777b25c8f8a472f36e35c5a88fc4", + "sha256": "ce928ad53d8497b6626ef3893cc0427c9ea36f0a63e82e2530179afa354eb15e", + "size": 111360, + "repo_path": "bios/Nintendo/Wii U/cafeLibs/snduser2.rpl", + "cores": [ + "cemu" + ], + "executable": true + }, + { + "dest": "Cemu/cafeLibs/swkbd.rpl", + "sha1": "4d88ddd169da56de721e4aa9e213a421f7221b4b", + "sha256": "94532b2639819e121ba69d75f5eabdf00988f87609c9ab9afe5ba1ca01eef4ea", + "size": 1116288, + "repo_path": "bios/Nintendo/Wii U/cafeLibs/swkbd.rpl", + "cores": [ + "cemu" + ], + "executable": true + }, + { + "dest": "Cemu/cafeLibs/erreula.rpl", + "sha1": "8c04ae8fea6e6d7431d9be8599377eca2dd690da", + "sha256": "f259a75dba800856afdda263664c6f8563b59692d5694e64d763654550d294fc", + "size": 790080, + "repo_path": "bios/Nintendo/Wii U/cafeLibs/erreula.rpl", + "cores": [ + "cemu" + ], + "executable": true + }, { "dest": "keys.txt", "sha1": "9edc52be45201ec99f6e4ceb5dc6abfc633c4eae", @@ -50464,6 +50619,72 @@ "microsoft-xbox" ] }, + { + "dest": "dolphin-emu/Sys/GC/dsp_rom.bin", + "sha1": "3c6cc6e04fdd0b2a392d7a6ed769455444846be7", + "sha256": "49d987ee1eab29a157425b82d54516957a81e1bac247c8834e494642605c3e8c", + "size": 8192, + "repo_path": "bios/Nintendo/GameCube/.variants/dsp_rom.bin.3c6cc6e0", + "cores": [ + "dolphin", + "ishiiruka" + ], + "systems": [ + "nintendo-gamecube" + ] + }, + { + "dest": "dolphin-emu/Sys/GC/dsp_coef.bin", + "sha1": "c116d867ba001dcd6bf6d399ff4bf38d340f556c", + "sha256": "a1e269cfdc6b82d5a55baab5a0bbbb3b9f953ad92729e0d5ec50dbc2746a2c47", + "size": 4096, + "repo_path": "bios/Nintendo/GameCube/.variants/dsp_coef.bin.c116d867", + "cores": [ + "dolphin", + "ishiiruka" + ], + "systems": [ + "nintendo-gamecube" + ] + }, + { + "dest": "dolphin-emu/Sys/GC/font_western.bin", + "sha1": "483a17ff25b68a8f18cae691da99f16beefbe370", + "sha256": "4ad991be2b0aa305f09b90a79fc50f57833b30b008890b5ef1336cc3d9d0bae0", + "size": 6478, + "repo_path": "bios/Other/dolphin-mmjr/font_western.bin", + "cores": [ + "dolphin", + "ishiiruka" + ], + "systems": [ + "nintendo-gamecube" + ] + }, + { + "dest": "dolphin-emu/Sys/GC/font_japanese.bin", + "sha1": "7fbbedcc7a1ae00414a4e9e967109cc78be1fe3b", + "sha256": "38f9f59d505bc4e2d86b0196706195f33ad72b7fe9d029cf263072cde19d044f", + "size": 259626, + "repo_path": "bios/Other/dolphin-mmjr/font_japanese.bin", + "cores": [ + "dolphin", + "ishiiruka" + ], + "systems": [ + "nintendo-gamecube" + ] + }, + { + "dest": "dolphin-emu/Sys/Triforce/segaboot.gcm", + "sha1": "7297a11be143011ceb75a0720f96855003415a5d", + "sha256": "9a2eee93f5ed961aea4b12a318e2b4a54fa69a31239c1378b8bef6ea02e90391", + "size": 2097152, + "repo_path": "bios/Sega/Triforce/segaboot.gcm", + "cores": [ + "dolphin" + ] + }, { "dest": "GC/dsp_rom.bin", "sha1": "3c6cc6e04fdd0b2a392d7a6ed769455444846be7", @@ -53367,58 +53588,6 @@ "hypseus-singe" ] }, - { - "dest": "dolphin-emu/Sys/GC/dsp_rom.bin", - "sha1": "3c6cc6e04fdd0b2a392d7a6ed769455444846be7", - "sha256": "49d987ee1eab29a157425b82d54516957a81e1bac247c8834e494642605c3e8c", - "size": 8192, - "repo_path": "bios/Nintendo/GameCube/.variants/dsp_rom.bin.3c6cc6e0", - "cores": [ - "ishiiruka" - ], - "systems": [ - "nintendo-gamecube" - ] - }, - { - "dest": "dolphin-emu/Sys/GC/dsp_coef.bin", - "sha1": "c116d867ba001dcd6bf6d399ff4bf38d340f556c", - "sha256": "a1e269cfdc6b82d5a55baab5a0bbbb3b9f953ad92729e0d5ec50dbc2746a2c47", - "size": 4096, - "repo_path": "bios/Nintendo/GameCube/.variants/dsp_coef.bin.c116d867", - "cores": [ - "ishiiruka" - ], - "systems": [ - "nintendo-gamecube" - ] - }, - { - "dest": "dolphin-emu/Sys/GC/font_western.bin", - "sha1": "483a17ff25b68a8f18cae691da99f16beefbe370", - "sha256": "4ad991be2b0aa305f09b90a79fc50f57833b30b008890b5ef1336cc3d9d0bae0", - "size": 6478, - "repo_path": "bios/Other/dolphin-mmjr/font_western.bin", - "cores": [ - "ishiiruka" - ], - "systems": [ - "nintendo-gamecube" - ] - }, - { - "dest": "dolphin-emu/Sys/GC/font_japanese.bin", - "sha1": "7fbbedcc7a1ae00414a4e9e967109cc78be1fe3b", - "sha256": "38f9f59d505bc4e2d86b0196706195f33ad72b7fe9d029cf263072cde19d044f", - "size": 259626, - "repo_path": "bios/Other/dolphin-mmjr/font_japanese.bin", - "cores": [ - "ishiiruka" - ], - "systems": [ - "nintendo-gamecube" - ] - }, { "dest": "jazz2/Source/AnimsSw.j2a", "sha1": "133392a2c638d17796c6f510cf94ec391cc4c407", @@ -61993,7 +62162,7 @@ ] }, { - "dest": "eeprom.bin", + "dest": "xbox_eeprom.bin", "sha1": "ff3e7eaf715fe5612e46fc984d686ed3b115baef", "sha256": "e652203e6d91140729c29588b18ff8838896cdeb9a3100fbf8d20763af27c9d2", "size": 256, @@ -62031,6 +62200,19 @@ "microsoft-xbox" ] }, + { + "dest": "eeprom.bin", + "sha1": "ff3e7eaf715fe5612e46fc984d686ed3b115baef", + "sha256": "e652203e6d91140729c29588b18ff8838896cdeb9a3100fbf8d20763af27c9d2", + "size": 256, + "repo_path": "bios/Microsoft/Xbox/xbox_eeprom.bin", + "cores": [ + "xemu" + ], + "systems": [ + "microsoft-xbox" + ] + }, { "dest": "cdb/cdb_rom.bin", "sha1": "b3c20fbe57cd2eb595e9edac86817e5948dccae4", @@ -62692,19 +62874,6 @@ "microsoft-xbox" ] }, - { - "dest": "GBA/gba_bios.bin", - "sha1": "300c20df6731a33952ded8c436f7f186d25d3492", - "sha256": "fd2547724b505f487e6dcb29ec2ecff3af35a841a77ab2e85fd87350abd36570", - "size": 16384, - "repo_path": "bios/Nintendo/Game Boy Advance/GBA_bios.rom", - "cores": [ - "dolphin" - ], - "systems": [ - "nintendo-gamecube" - ] - }, { "dest": "dolphin-emu/Sys/GC/USA/IPL.bin", "sha1": "ef9194ab4804aa0aa8540d846caf291b28331165", @@ -62712,6 +62881,7 @@ "size": 2097152, "repo_path": "bios/Nintendo/GameCube/GC/USA/IPL.bin", "cores": [ + "dolphin", "ishiiruka" ], "systems": [ @@ -62721,7 +62891,7 @@ "north-america" ], "region_groups": [ - "Ishiiruka:file:dolphin-emu/Sys/GC/USA/IPL.bin" + "Dolphin:file:dolphin-emu/Sys/GC/USA/IPL.bin" ] }, { @@ -62731,6 +62901,7 @@ "size": 2097152, "repo_path": "bios/Nintendo/GameCube/GC/EUR/IPL.bin", "cores": [ + "dolphin", "ishiiruka" ], "systems": [ @@ -62741,7 +62912,7 @@ "oceania" ], "region_groups": [ - "Ishiiruka:file:dolphin-emu/Sys/GC/EUR/IPL.bin" + "Dolphin:file:dolphin-emu/Sys/GC/EUR/IPL.bin" ] }, { @@ -62751,6 +62922,7 @@ "size": 2097152, "repo_path": "bios/Nintendo/GameCube/GC/JAP/IPL.bin", "cores": [ + "dolphin", "ishiiruka" ], "systems": [ @@ -62760,7 +62932,20 @@ "japan" ], "region_groups": [ - "Ishiiruka:file:dolphin-emu/Sys/GC/JAP/IPL.bin" + "Dolphin:file:dolphin-emu/Sys/GC/JAP/IPL.bin" + ] + }, + { + "dest": "GBA/gba_bios.bin", + "sha1": "300c20df6731a33952ded8c436f7f186d25d3492", + "sha256": "fd2547724b505f487e6dcb29ec2ecff3af35a841a77ab2e85fd87350abd36570", + "size": 16384, + "repo_path": "bios/Nintendo/Game Boy Advance/GBA_bios.rom", + "cores": [ + "dolphin" + ], + "systems": [ + "nintendo-gamecube" ] }, { diff --git a/platforms/batocera.yml b/platforms/batocera.yml index bd154f86..b38d0352 100644 --- a/platforms/batocera.yml +++ b/platforms/batocera.yml @@ -5803,3 +5803,17 @@ standalone_cores: - xvic - ymir - yquake2 +dual_mode_cores: +- applewin +- dolphin +- easyrpg +- flycast +- hatari +- mame +- melonds +- pcsx2 +- play +- ppsspp +- scummvm +- stella +- tic80 diff --git a/platforms/recalbox.yml b/platforms/recalbox.yml index 8c30559a..930608e5 100644 --- a/platforms/recalbox.yml +++ b/platforms/recalbox.yml @@ -262,6 +262,16 @@ standalone_cores: - xroar - xscpu64 - xvic +dual_mode_cores: +- amiberry +- dolphin +- hatari +- mupen64plus +- pcsx2 +- pcsx_rearmed +- ppsspp +- rb5000 +- scummvm systems: commodore-amiga: files: diff --git a/platforms/retrobat.yml b/platforms/retrobat.yml index c47c3547..ccb9bbfc 100644 --- a/platforms/retrobat.yml +++ b/platforms/retrobat.yml @@ -421,6 +421,27 @@ standalone_cores: - zesarux - zinc - zquestclassic +dual_mode_cores: +- amiberry +- applewin +- azahar +- desmume +- dolphin +- fbneo +- flycast +- hatari +- kronos +- melonds +- mesen +- mgba +- pcsx2 +- ppsspp +- scummvm +- snes9x +- stella +- supermodel +- yabasanshiro +- ymir systems: 3do: files: diff --git a/platforms/rocknix.yml b/platforms/rocknix.yml index f04670cc..d4cde756 100644 --- a/platforms/rocknix.yml +++ b/platforms/rocknix.yml @@ -134,6 +134,70 @@ standalone_cores: - xemu-sa - yabasanshiro - yabasanshiro-sa +dual_mode_cores: +- aethersx2 +- aethersx2-sa +- amiberry +- ares +- ares-sa +- armsx2 +- armsx2-sa +- azahar +- azahar-sa +- bigpemu +- bigpemu-sa +- cemu +- cemu-sa +- daedalusx64 +- daedalusx64-sa +- dolphin +- dolphin-sa +- drastic +- drastic-sa +- dsperate +- dsperate-sa +- duckstation +- duckstation-sa +- flycast +- flycast-sa +- gopher64 +- gopher64-sa +- gzdoom +- gzdoom-sa +- hatari +- hatarisa +- heroic +- hypseus-singe +- mednafen +- melonds +- melonds-sa +- mupen64plus +- mupen64plus-sa +- nanoboyadvance +- nanoboyadvance-sa +- openbor +- pico-8 +- ppsspp +- ppsspp-sa +- rpcs3 +- rpcs3-sa +- scummvm +- scummvm-sa +- skyemu +- skyemu-sa +- steam +- supermodel +- supermodel-sa +- touchhle +- touchhle-sa +- vice +- vice-sa +- vita3k +- vita3k-sa +- xemu +- xemu-sa +- yabasanshiro +- yabasanshiro-sa systems: microsoft-xbox: files: diff --git a/schemas/platform.schema.json b/schemas/platform.schema.json index 30518648..215cfa39 100644 --- a/schemas/platform.schema.json +++ b/schemas/platform.schema.json @@ -45,6 +45,14 @@ }, "uniqueItems": true }, + "dual_mode_cores": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true, + "description": "Emulators of standalone_cores the platform also launches as a libretro core: both layouts get their files" + }, "homepage": { "type": "string", "format": "uri", diff --git a/scripts/common.py b/scripts/common.py index df830466..b7932804 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -1861,6 +1861,7 @@ def group_identical_platforms( emulators = [ cores if isinstance(cores, str) else sorted(map(str, cores or [])), sorted(map(str, config.get("standalone_cores") or [])), + sorted(map(str, config.get("dual_mode_cores") or [])), # One archive cannot serve a filesystem that folds case and one # that does not: Lakka keeps bios.col beside BIOS.col. bool(config.get("case_insensitive_fs")), @@ -2004,6 +2005,44 @@ def runs_standalone( ) +# Marks, in a platform's standalone set, an emulator it also launches as a +# libretro core. No core name starts with it. +DUAL_MODE_PREFIX = "dual:" + + +def standalone_set(config: dict) -> set[str]: + """The emulators a platform launches standalone, with its dual-mode ones. + + ``dual_mode_cores`` names those it offers both ways (Batocera lists + libretro:dolphin and standalone dolphin for GameCube). They travel in the + same set, tagged, so every reader of standalone_cores sees them. + """ + names = {str(c) for c in config.get("standalone_cores") or []} + names |= {DUAL_MODE_PREFIX + str(c) for c in config.get("dual_mode_cores") or []} + return names + + +def platform_layouts( + emu_name: str, profile: dict, standalone_cores: set[str] +) -> list[tuple[dict, bool]]: + """The layouts a platform gives an emulator: (profile, standalone?) each. + + One, libretro or standalone, for most. Both for an emulator the platform + launches either way: laid out standalone only, Batocera's libretro + Dolphin found dolphin-emu/Sys/GC/ empty. Each layout keeps its own rules + (what the build reads from the system directory, where it reads it). + """ + if not runs_standalone(emu_name, profile, standalone_cores): + return [(profile, False)] + dual = {c.removeprefix(DUAL_MODE_PREFIX) for c in standalone_cores + if c.startswith(DUAL_MODE_PREFIX)} + if "libretro" in str(profile.get("type", "")) and dual & { + emu_name, *(str(c) for c in profile.get("cores", [])) + }: + return [(profile, False), (profile, True)] + return [(profile, True)] + + # What identifies a profile entry's content, carried unchanged wherever the # entry travels (cross-reference report, core extras, second-pass copies). # Each copy once kept its own subset: np2kai's bios.rom lost min_size and diff --git a/scripts/cross_reference.py b/scripts/cross_reference.py index 9b99d904..215566b1 100644 --- a/scripts/cross_reference.py +++ b/scripts/cross_reference.py @@ -38,7 +38,8 @@ from common import ( require_platform, require_yaml, resolve_local_file, - runs_standalone, + platform_layouts, + standalone_set, select_emulator_profiles, ) from validation import declaration_settles, outside_gap_scope @@ -502,7 +503,10 @@ def _cross_reference_profile( # The build a platform runs decides which entries it reads, as in # find_undeclared_files: without a platform, the libretro build. - is_standalone = runs_standalone(emu_name, profile, standalone_cores or set()) + layouts = platform_layouts(emu_name, profile, standalone_cores or set()) + # Each file under each layout the platform gives the emulator: both, for + # one it launches as a libretro core and standalone. + layout_files = [(f, standalone) for view, standalone in layouts for f in view.get("files", [])] gaps = [] covered = [] unsourceable_list: list[dict] = [] @@ -516,7 +520,7 @@ def _cross_reference_profile( for f in emu_files: versions.setdefault(slot_key(f), []).append(f) - for f in emu_files: + for f, is_standalone in layout_files: fname = f.get("name", "") # The destination the running build reads, as find_undeclared_files # takes it: Batocera's standalone Cemu reads keys.txt at @@ -527,6 +531,9 @@ def _cross_reference_profile( else: effective_path = f.get("path") or fname seen_key = slot_key(f) + if len(layouts) > 1: + # One file read at one path by both builds counts once. + seen_key = (effective_path.casefold(), seen_key) if not fname or seen_key in seen_files: continue @@ -820,7 +827,7 @@ def main() -> None: config = load_platform_config(args.platform, args.platforms_dir) relevant = resolve_platform_cores(config, profiles, target_cores=target_cores) profiles = {k: v for k, v in profiles.items() if k in relevant} - standalone_cores = set(map(str, config.get("standalone_cores", []))) + standalone_cores = standalone_set(config) if not profiles: if args.emulator: diff --git a/scripts/packextras.py b/scripts/packextras.py index 49ad515f..8c81c451 100644 --- a/scripts/packextras.py +++ b/scripts/packextras.py @@ -15,7 +15,8 @@ from common import load_emulator_profiles from common import resolution_is_hash_exact from common import resolve_local_file from common import resolve_platform_cores -from common import runs_standalone +from common import platform_layouts +from common import standalone_set as platform_standalone_set from common import sanitize_pack_path from common import size_fits from validation import read_from_system_dir @@ -290,96 +291,96 @@ def _agnostic_scan_extras( agnostic_votes.get(directory + "/", 0) + 1 ) - standalone = runs_standalone(emu_name, profile, standalone_set or set()) - for f in profile.get("files", []): - if not is_profile_agnostic and not f.get("agnostic"): - continue - fname = f.get("name", "") - if not fname: - continue - # A file of the build the platform does not run seeds nothing: - # armsx2's proverb.zip is fetched by the Android app into its own - # arcade/ folder, and the libretro packs carried it at system/. - mode = f.get("mode") - if (mode == "standalone" and not standalone) or ( - mode == "libretro" and standalone - ): - continue - # An agnostic BIOS mode says the BIOS filename is free, not that - # every file the emulator loads is. A free filename still has a - # known shape, and that shape is what identifies a candidate: an - # entry declaring no size takes the whole directory, which is how - # the flag icons of an Android package walked into a PS2 pack. - if f.get("category", "bios") != "bios": - continue - if not (f.get("size") or f.get("min_size") or f.get("max_size")): - continue - # The free name is the BIOS directory's: a shader, a sound or a - # font the emulator reads from its resources tree has a fixed - # name and must not seed a scan (yaps2 flattened 189 of them to - # the root of the RetroArch pack). - bios_dir = profile.get("bios_directory") - fpath = f.get("path") or "" - if bios_dir and "/" in fpath and not fpath.startswith(bios_dir): - continue + for profile, standalone in platform_layouts(emu_name, profile, standalone_set or set()): + for f in profile.get("files", []): + if not is_profile_agnostic and not f.get("agnostic"): + continue + fname = f.get("name", "") + if not fname: + continue + # A file of the build the platform does not run seeds nothing: + # armsx2's proverb.zip is fetched by the Android app into its own + # arcade/ folder, and the libretro packs carried it at system/. + mode = f.get("mode") + if (mode == "standalone" and not standalone) or ( + mode == "libretro" and standalone + ): + continue + # An agnostic BIOS mode says the BIOS filename is free, not that + # every file the emulator loads is. A free filename still has a + # known shape, and that shape is what identifies a candidate: an + # entry declaring no size takes the whole directory, which is how + # the flag icons of an Android package walked into a PS2 pack. + if f.get("category", "bios") != "bios": + continue + if not (f.get("size") or f.get("min_size") or f.get("max_size")): + continue + # The free name is the BIOS directory's: a shader, a sound or a + # font the emulator reads from its resources tree has a fixed + # name and must not seed a scan (yaps2 flattened 189 of them to + # the root of the RetroArch pack). + bios_dir = profile.get("bios_directory") + fpath = f.get("path") or "" + if bios_dir and "/" in fpath and not fpath.startswith(bios_dir): + continue - path_prefix = resolved_dirs.get(id(f), "") - if not path_prefix: - continue - # A name matching one file identifies it. A name matching several - # identifies nothing on its own, so the profile's other files have - # to agree on the directory before the scan walks it. - ambiguous = id(f) in named_only and len(by_name.get(fname, [])) > 1 - if ambiguous and agnostic_votes.get(path_prefix, 0) < 2: - continue + path_prefix = resolved_dirs.get(id(f), "") + if not path_prefix: + continue + # A name matching one file identifies it. A name matching several + # identifies nothing on its own, so the profile's other files have + # to agree on the directory before the scan walks it. + ambiguous = id(f) in named_only and len(by_name.get(fname, [])) > 1 + if ambiguous and agnostic_votes.get(path_prefix, 0) < 2: + continue - # Scan DB for all files under this prefix matching size - for sha1, entry in files_db.items(): - path = entry.get("path", "") - if not path.startswith(path_prefix): - continue - if not size_fits(f, entry.get("size", 0)): - continue - scan_name = entry.get("name", "") - if not scan_name: - continue - # Into the directory the seed entry names, not the root: - # yaps2 reads its BIOS from pcsx2/bios/. - seed = f.get("path") or "" - if standalone: - seed = f.get("standalone_path") or seed - folder = seed.rsplit("/", 1)[0] if "/" in seed else "" - dest = f"{folder}/{scan_name}" if folder else scan_name - full_dest = f"{extras_prefix}/{dest}" if extras_prefix else dest - if full_dest in seen_dests and (taken is None or full_dest not in taken): - continue - extra = { - "name": scan_name, - "destination": dest, - # The scan already holds the entry it selected, so it - # names it by content. Emitting the filename alone - # sent the packing step back to a by-name lookup, and - # three PS2 resources came back from another - # emulator's copy of the same filename. - "sha1": sha1, - "required": False, - "hle_fallback": False, - "source_emulator": profile.get("emulator", emu_name), - "source_profile": emu_name, - "source_system": f.get("system"), - "source_systems": list(profile.get("systems", [])), - "region": f.get("region"), - "variant_group": f.get("variant_group"), - "agnostic_scan": True, - } - extra["claim_order"] = (4, 0, emu_name) - if not _take(extra, full_dest, seen_dests, taken): - continue - extras.append(extra) - extras.extend(_companion_extras( - _companions(path, scan_name, _companion_extensions(profile, fname), files_db), - folder, extras_prefix, seen_dests, profile, emu_name, f, taken, - )) + # Scan DB for all files under this prefix matching size + for sha1, entry in files_db.items(): + path = entry.get("path", "") + if not path.startswith(path_prefix): + continue + if not size_fits(f, entry.get("size", 0)): + continue + scan_name = entry.get("name", "") + if not scan_name: + continue + # Into the directory the seed entry names, not the root: + # yaps2 reads its BIOS from pcsx2/bios/. + seed = f.get("path") or "" + if standalone: + seed = f.get("standalone_path") or seed + folder = seed.rsplit("/", 1)[0] if "/" in seed else "" + dest = f"{folder}/{scan_name}" if folder else scan_name + full_dest = f"{extras_prefix}/{dest}" if extras_prefix else dest + if full_dest in seen_dests and (taken is None or full_dest not in taken): + continue + extra = { + "name": scan_name, + "destination": dest, + # The scan already holds the entry it selected, so it + # names it by content. Emitting the filename alone + # sent the packing step back to a by-name lookup, and + # three PS2 resources came back from another + # emulator's copy of the same filename. + "sha1": sha1, + "required": False, + "hle_fallback": False, + "source_emulator": profile.get("emulator", emu_name), + "source_profile": emu_name, + "source_system": f.get("system"), + "source_systems": list(profile.get("systems", [])), + "region": f.get("region"), + "variant_group": f.get("variant_group"), + "agnostic_scan": True, + } + extra["claim_order"] = (4, 0, emu_name) + if not _take(extra, full_dest, seen_dests, taken): + continue + extras.append(extra) + extras.extend(_companion_extras( + _companions(path, scan_name, _companion_extensions(profile, fname), files_db), + folder, extras_prefix, seen_dests, profile, emu_name, f, taken, + )) return extras @@ -701,7 +702,7 @@ def _collect_emulator_extras( return extras relevant = resolve_platform_cores(config, profiles, target_cores=target_cores) - standalone_set = {str(c) for c in config.get("standalone_cores", [])} + standalone_set = platform_standalone_set(config) by_name = db.get("indexes", {}).get("by_name", {}) by_path_suffix = db.get("indexes", {}).get("by_path_suffix", {}) @@ -716,73 +717,73 @@ def _collect_emulator_extras( continue if emu_name not in relevant: continue - is_standalone = runs_standalone(emu_name, profile, standalone_set) - for f in profile.get("files", []): - fname = f.get("name", "") - if not fname: - continue - # Only duplicate files already covered at another destination. - # An unsourceable entry is a file no collection holds: the name - # covered elsewhere belongs to another program's file. - if fname not in covered_names or f.get("unsourceable"): - continue - # A member of an archive is read inside it, never as a loose - # file; the archive itself is placed by the passes above. - if f.get("archive"): - continue - # No path: the core reads it from nowhere a pack reaches - # (Dolphin's codehandler.bin ships in its Sys tree). - if "path" in f and f["path"] is None: - continue - file_mode = f.get("mode") - if file_mode == "standalone" and not is_standalone: - continue - if file_mode == "libretro" and is_standalone: - continue - if not read_from_system_dir(f, is_standalone): - continue - if is_standalone: - raw = f.get("standalone_path") or f.get("path") or fname - else: - raw = f.get("path") or fname - dest = f"{raw}{fname}" if raw.endswith("/") else raw - # A bare name is a destination too: the core reads it at the root - # of the directory core files go to, which the platform may not - # declare (Recalbox has its System Cards under pcenginecd/ only, - # geargrafx reads them from the root). - full_dest = f"{extras_prefix}/{dest}" if extras_prefix else dest - if full_dest in seen_dests and full_dest not in taken: - continue - # Check file exists in repo or data dirs - if not ( - by_name.get(fname) - or by_name.get(dest.rsplit("/", 1)[-1]) - or by_path_suffix.get(dest) - ): - continue - extra = { - "name": fname, - "destination": dest, - "required": f.get("required", False), - "hle_fallback": f.get("hle_fallback", False), - "source_emulator": profile.get("emulator", emu_name), - "source_profile": emu_name, - "source_system": f.get("system"), - "source_systems": list(profile.get("systems", [])), - "region": f.get("region"), - "variant_group": f.get("variant_group"), - } - # The same identity the first pass carries: without it the copy - # resolves on its name alone and a same-named file of another - # game fills the slot the profile marks as not held. - for identity in PROFILE_IDENTITY_FIELDS: - if f.get(identity) not in (None, "", []): - extra[identity] = f[identity] - if f.get("contents"): - extra["contents"] = f["contents"] - extra["claim_order"] = (2, 0, emu_name) - if _take(extra, full_dest, seen_dests, taken): - extras.append(extra) + for profile, is_standalone in platform_layouts(emu_name, profile, standalone_set): + for f in profile.get("files", []): + fname = f.get("name", "") + if not fname: + continue + # Only duplicate files already covered at another destination. + # An unsourceable entry is a file no collection holds: the name + # covered elsewhere belongs to another program's file. + if fname not in covered_names or f.get("unsourceable"): + continue + # A member of an archive is read inside it, never as a loose + # file; the archive itself is placed by the passes above. + if f.get("archive"): + continue + # No path: the core reads it from nowhere a pack reaches + # (Dolphin's codehandler.bin ships in its Sys tree). + if "path" in f and f["path"] is None: + continue + file_mode = f.get("mode") + if file_mode == "standalone" and not is_standalone: + continue + if file_mode == "libretro" and is_standalone: + continue + if not read_from_system_dir(f, is_standalone): + continue + if is_standalone: + raw = f.get("standalone_path") or f.get("path") or fname + else: + raw = f.get("path") or fname + dest = f"{raw}{fname}" if raw.endswith("/") else raw + # A bare name is a destination too: the core reads it at the root + # of the directory core files go to, which the platform may not + # declare (Recalbox has its System Cards under pcenginecd/ only, + # geargrafx reads them from the root). + full_dest = f"{extras_prefix}/{dest}" if extras_prefix else dest + if full_dest in seen_dests and full_dest not in taken: + continue + # Check file exists in repo or data dirs + if not ( + by_name.get(fname) + or by_name.get(dest.rsplit("/", 1)[-1]) + or by_path_suffix.get(dest) + ): + continue + extra = { + "name": fname, + "destination": dest, + "required": f.get("required", False), + "hle_fallback": f.get("hle_fallback", False), + "source_emulator": profile.get("emulator", emu_name), + "source_profile": emu_name, + "source_system": f.get("system"), + "source_systems": list(profile.get("systems", [])), + "region": f.get("region"), + "variant_group": f.get("variant_group"), + } + # The same identity the first pass carries: without it the copy + # resolves on its name alone and a same-named file of another + # game fills the slot the profile marks as not held. + for identity in PROFILE_IDENTITY_FIELDS: + if f.get(identity) not in (None, "", []): + extra[identity] = f[identity] + if f.get("contents"): + extra["contents"] = f["contents"] + extra["claim_order"] = (2, 0, emu_name) + if _take(extra, full_dest, seen_dests, taken): + extras.append(extra) extras.extend( _archive_prefix_extras( diff --git a/scripts/safeparse.py b/scripts/safeparse.py index 83dab9da..8411b714 100644 --- a/scripts/safeparse.py +++ b/scripts/safeparse.py @@ -97,3 +97,15 @@ def yaml_load(stream: str | TextIO) -> Any: if yaml is None: return require_yaml() # exits with the install hint return yaml.load(stream, Loader=_YAML_LOADER) + + +def yaml_load_upstream(stream: str | TextIO) -> Any: + """Parse a third party's YAML the way its own PyYAML code reads it. + + Our files refuse a repeated key; a platform's file is data we transcribe, + and its code keeps the last value. Batocera 43.1's configgen-defaults.yml + names rtcw's core twice, and refusing it stopped the rescrape. + """ + if yaml is None: + return require_yaml() + return yaml.load(stream, Loader=getattr(yaml, "CSafeLoader", None) or yaml.SafeLoader) diff --git a/scripts/scraper/batocera_scraper.py b/scripts/scraper/batocera_scraper.py index ea2e4d7b..7e68b898 100644 --- a/scripts/scraper/batocera_scraper.py +++ b/scripts/scraper/batocera_scraper.py @@ -18,6 +18,7 @@ from pathlib import Path from common import yaml_load +from safeparse import yaml_load_upstream from .base_scraper import ( BaseScraper, @@ -98,7 +99,7 @@ def es_systems_emulators(text: str) -> tuple[set[str], set[str]]: reading it alone left DOSBox Staging, melonDS DS and nine other built cores outside the pack. """ - data = yaml_load(text) + data = yaml_load_upstream(text) if not isinstance(data, dict): raise ValueError("es_systems.yml is not a mapping") cores: set[str] = set() @@ -126,6 +127,20 @@ def es_systems_emulators(text: str) -> tuple[set[str], set[str]]: return cores, standalone +def es_systems_libretro(text: str) -> set[str]: + """The cores es_systems.yml offers under `libretro:` for some system.""" + data = yaml_load_upstream(text) + if not isinstance(data, dict): + raise ValueError("es_systems.yml is not a mapping") + found: set[str] = set() + for system in data.values(): + emulators = system.get("emulators") if isinstance(system, dict) else None + libretro = emulators.get("libretro") if isinstance(emulators, dict) else None + if isinstance(libretro, dict): + found.update(str(core) for core, spec in libretro.items() if isinstance(spec, dict)) + return found + + SYSTEM_SLUG_MAP = { "advision": "entex-advision", "amiga500": "commodore-amiga", @@ -253,11 +268,12 @@ class Scraper(BaseScraper): self.tag = fetch_stable_tag() super().__init__(url=SOURCE_URL.format(tag=self.tag)) - def _fetch_cores(self) -> tuple[list[str], list[str]]: + def _fetch_cores(self) -> tuple[list[str], list[str], list[str]]: """Extract core names and standalone cores from configgen-defaults.yml. - Returns (all_cores, standalone_cores) where standalone_cores are - those with emulator != "libretro". + Returns (all_cores, standalone_cores, dual_mode_cores): standalone + cores are those with emulator != "libretro", dual-mode ones those + es_systems.yml also offers under libretro (dolphin for GameCube). """ url = CONFIGGEN_DEFAULTS_URL.format(tag=self.tag) try: @@ -271,10 +287,10 @@ class Scraper(BaseScraper): raise ConnectionError( f"Failed to fetch {url}: {e}" ) from e - data = yaml_load(raw) - cores, standalone = es_systems_emulators( - self._fetch_text(ES_SYSTEMS_URL.format(tag=self.tag)) - ) + data = yaml_load_upstream(raw) + es_systems = self._fetch_text(ES_SYSTEMS_URL.format(tag=self.tag)) + cores, standalone = es_systems_emulators(es_systems) + libretro = es_systems_libretro(es_systems) if not cores: raise RuntimeError("es_systems.yml names no emulator") for system, cfg in data.items(): @@ -289,7 +305,8 @@ class Scraper(BaseScraper): if core and core != emulator: standalone.add(core) dropped = _not_cores() - return sorted(cores - dropped), sorted(standalone - dropped) + dual = (standalone & libretro) - dropped + return sorted(cores - dropped), sorted(standalone - dropped), sorted(dual) def _fetch_text(self, url: str) -> str: req = urllib.request.Request(url, headers={"User-Agent": "retrobios-scraper/1.0"}) @@ -449,7 +466,7 @@ class Scraper(BaseScraper): systems[req.system]["files"].append(requirement_entry(req)) - cores, standalone = self._fetch_cores() + cores, standalone, dual = self._fetch_cores() result = { "platform": "Batocera", "version": self.tag.removeprefix("batocera-"), @@ -463,6 +480,9 @@ class Scraper(BaseScraper): } if standalone: result["standalone_cores"] = standalone + if dual: + # Launched both ways: each layout gets its files. + result["dual_mode_cores"] = dual return result diff --git a/scripts/scraper/recalbox_scraper.py b/scripts/scraper/recalbox_scraper.py index cdaeb5dc..c37a912f 100644 --- a/scripts/scraper/recalbox_scraper.py +++ b/scripts/scraper/recalbox_scraper.py @@ -153,6 +153,18 @@ def _core_names(values: dict[str, str]) -> tuple[set[str], set[str]]: return names, set(names) +def system_ini_libretro(text: str) -> set[str]: + """The libretro cores one Recalbox system.ini offers.""" + import re + + return { + values.get("core", "") + for section, values in _ini_sections(text).items() + if re.fullmatch(r"core\.\d+(\.[\w-]+)?", section) + and values.get("emulator") == "libretro" and values.get("core") + } + + def system_ini_cores(text: str) -> tuple[set[str], set[str]]: """The cores one Recalbox system.ini offers, and those run standalone. @@ -227,8 +239,9 @@ class Scraper(BaseScraper): raise RuntimeError(f"no system.ini at {self.tag} for {', '.join(missing[:5])}") return [texts[path] for path in paths] - def _fetch_cores(self) -> tuple[list[str], list[str]]: - """Every core Recalbox's systems offer, and those it runs standalone. + def _fetch_cores(self) -> tuple[list[str], list[str], list[str]]: + """Every core Recalbox's systems offer, those it runs standalone, and + those it offers both as a libretro core and standalone. Read from each system.ini of recalbox-romfs2 at the scraped tag: the systems ES lists and the cores a user can pick for each. @@ -250,13 +263,15 @@ class Scraper(BaseScraper): raise RuntimeError(f"no Recalbox system found at {self.tag}") cores: set[str] = set() standalone: set[str] = set() + libretro: set[str] = set() for text in self._system_inis(systems): found, alone = system_ini_cores(text) cores |= found standalone |= alone + libretro |= system_ini_libretro(text) if not cores: raise RuntimeError(f"no core in Recalbox's system.ini files at {self.tag}") - return sorted(cores), sorted(standalone) + return sorted(cores), sorted(standalone), sorted(libretro & standalone) def fetch_requirements(self) -> list[BiosRequirement]: """Parse es_bios.xml and return BIOS requirements.""" @@ -350,7 +365,7 @@ class Scraper(BaseScraper): systems[req.system]["files"].append(requirement_entry(req)) - cores, standalone = self._fetch_cores() + cores, standalone, dual = self._fetch_cores() return { "platform": "Recalbox", "version": self.tag, @@ -361,6 +376,8 @@ class Scraper(BaseScraper): "verification_mode": "md5", "cores": cores, "standalone_cores": standalone, + # Launched both ways: each layout gets its files. + **({"dual_mode_cores": dual} if dual else {}), "systems": systems, } diff --git a/scripts/scraper/retrobat_scraper.py b/scripts/scraper/retrobat_scraper.py index 0ee0e95f..c7b0736b 100644 --- a/scripts/scraper/retrobat_scraper.py +++ b/scripts/scraper/retrobat_scraper.py @@ -44,6 +44,21 @@ ES_SYSTEMS_URL = ( ) +def es_systems_dual(text: str) -> set[str]: + """Emulators es_systems.cfg offers both as a libretro core and standalone.""" + from common import parse_untrusted_xml + + libretro: set[str] = set() + standalone: set[str] = set() + for emulator in parse_untrusted_xml(text, "es_systems.cfg").iter("emulator"): + name = (emulator.get("name") or "").strip() + if name == "libretro": + libretro |= {(core.text or "").strip() for core in emulator.iter("core")} - {""} + elif name: + standalone.add(name) + return libretro & standalone + + def es_systems_cores(text: str) -> tuple[set[str], set[str]]: """Every core RetroBat's ES offers, and the emulators it runs standalone. @@ -207,7 +222,9 @@ class Scraper(BaseScraper): req = urllib.request.Request(es_url, headers={"User-Agent": "retrobios-scraper/1.0"}) try: with urllib.request.urlopen(req, timeout=30) as resp: - cores, standalone = es_systems_cores(resp.read().decode("utf-8")) + es_text = resp.read().decode("utf-8") + cores, standalone = es_systems_cores(es_text) + dual = es_systems_dual(es_text) except urllib.error.URLError as e: raise ConnectionError(f"Failed to fetch {es_url}: {e}") from e if not cores: @@ -223,6 +240,8 @@ class Scraper(BaseScraper): "verification_mode": "md5", "cores": sorted(cores), "standalone_cores": sorted(standalone), + # Launched both ways: each layout gets its files. + **({"dual_mode_cores": sorted(dual)} if dual else {}), "systems": systems, } diff --git a/scripts/scraper/rocknix_scraper.py b/scripts/scraper/rocknix_scraper.py index 292ef9b1..4fc23911 100644 --- a/scripts/scraper/rocknix_scraper.py +++ b/scripts/scraper/rocknix_scraper.py @@ -236,6 +236,9 @@ class Scraper(BaseScraper): # emulators ROCKNIX packages beside it. "cores": ["retroarch", *standalone], "standalone_cores": standalone, + # RetroArch carries every libretro core beside them, so an + # emulator with a libretro build is launched both ways. + "dual_mode_cores": list(standalone), "systems": systems, } diff --git a/scripts/scraper/targets/batocera_targets_scraper.py b/scripts/scraper/targets/batocera_targets_scraper.py index de8df37a..a9ff2969 100644 --- a/scripts/scraper/targets/batocera_targets_scraper.py +++ b/scripts/scraper/targets/batocera_targets_scraper.py @@ -23,7 +23,7 @@ from datetime import datetime, timezone import yaml -from common import yaml_load +from safeparse import yaml_load_upstream from . import BaseTargetScraper @@ -244,7 +244,7 @@ def _parse_es_systems(text: str) -> dict[str, list[str]]: : {requireAnyOf: [BR2_PACKAGE_FOO]} """ try: - data = yaml_load(text) + data = yaml_load_upstream(text) except yaml.YAMLError: return {} diff --git a/scripts/slots.py b/scripts/slots.py index eaa48148..0cc8f949 100644 --- a/scripts/slots.py +++ b/scripts/slots.py @@ -32,7 +32,8 @@ from common import ( resolution_is_hash_exact, resolve_local_file, resolve_platform_cores, - runs_standalone, + platform_layouts, + standalone_set as platform_standalone_set, variant_base_name, ) from packresolve import preferred_entries @@ -202,63 +203,63 @@ def profile_claims( # carries its scan candidates, never its named entries. if profile.get("bios_mode") == "agnostic": continue - is_standalone = runs_standalone(emu_name, profile, standalone_cores) - claims.extend( - _archive_claims( - emu_name, profile, is_standalone, db, base_dest, placement, - zip_contents, data_dir_registry, - ) - ) - for entry in profile.get("files") or []: - if not isinstance(entry, dict): - continue - # The same rule as the gap report and the builder: an entry the - # pack never carries (read from the save directory, a - # placeholder, a filename-agnostic scan) arbitrates nothing. - if outside_gap_scope(entry, is_standalone): - continue - # A member of a romset is not a file at the BIOS root: the archive - # holding it occupies the destination (_archive_claims). Claiming - # the member put FBNeo's msx.zip contents loose at the root. - if entry.get("archive"): - continue - dest = ( - (entry.get("standalone_path") or entry.get("path")) - if is_standalone - else entry.get("path") - ) or entry.get("name") or "" - if not dest: - continue - if placement is None: - full = f"{base_dest}/{dest}" if base_dest else dest - else: - placed = placement.place( - {"profile": emu_name, "system": entry.get("system")}, dest + for profile, is_standalone in platform_layouts(emu_name, profile, standalone_cores): + claims.extend( + _archive_claims( + emu_name, profile, is_standalone, db, base_dest, placement, + zip_contents, data_dir_registry, ) - if placed is None: + ) + for entry in profile.get("files") or []: + if not isinstance(entry, dict): continue - full = placed[1] - # The owner's own copy is the one the builder ships. - local, status = resolve_local_file( - {**entry, "source_profile": emu_name}, - db, - zip_contents, - dest_hint=dest, - data_dir_registry=data_dir_registry, - ) - if status == "name_exact" and _size_the_code_reads(entry, local): - status = SIZE_EXACT - claims.append( - Claim( - origin="profile", - destination=full, - name=entry.get("name", ""), - emulator=emu_name, - entry=entry, - local_path=local, - status=status, + # The same rule as the gap report and the builder: an entry the + # pack never carries (read from the save directory, a + # placeholder, a filename-agnostic scan) arbitrates nothing. + if outside_gap_scope(entry, is_standalone): + continue + # A member of a romset is not a file at the BIOS root: the archive + # holding it occupies the destination (_archive_claims). Claiming + # the member put FBNeo's msx.zip contents loose at the root. + if entry.get("archive"): + continue + dest = ( + (entry.get("standalone_path") or entry.get("path")) + if is_standalone + else entry.get("path") + ) or entry.get("name") or "" + if not dest: + continue + if placement is None: + full = f"{base_dest}/{dest}" if base_dest else dest + else: + placed = placement.place( + {"profile": emu_name, "system": entry.get("system")}, dest + ) + if placed is None: + continue + full = placed[1] + # The owner's own copy is the one the builder ships. + local, status = resolve_local_file( + {**entry, "source_profile": emu_name}, + db, + zip_contents, + dest_hint=dest, + data_dir_registry=data_dir_registry, + ) + if status == "name_exact" and _size_the_code_reads(entry, local): + status = SIZE_EXACT + claims.append( + Claim( + origin="profile", + destination=full, + name=entry.get("name", ""), + emulator=emu_name, + entry=entry, + local_path=local, + status=status, + ) ) - ) return claims @@ -524,7 +525,7 @@ def slot_choices( platform_profiles, db, config.get("base_destination", ""), - {str(c) for c in config.get("standalone_cores", [])}, + platform_standalone_set(config), zip_contents, data_dir_registry, ) @@ -821,7 +822,7 @@ def scan_platform( relevant, db, config.get("base_destination", ""), - {str(c) for c in config.get("standalone_cores", [])}, + platform_standalone_set(config), build_zip_contents_index(db), load_data_dir_registry(platforms_dir), ) diff --git a/scripts/truth.py b/scripts/truth.py index 5e45d3e4..6152b8e6 100644 --- a/scripts/truth.py +++ b/scripts/truth.py @@ -8,7 +8,12 @@ from __future__ import annotations import sys -from common import _norm_system_id, resolve_platform_cores, runs_standalone +from common import ( + _norm_system_id, + platform_layouts, + resolve_platform_cores, + standalone_set as platform_standalone_set, +) from hashing import parse_md5_list from validation import filter_files_by_mode, read_from_system_dir @@ -395,7 +400,7 @@ def generate_platform_truth( # The layout rule verify and the pack builder apply: truth guessed its # own from the profile type where the platform names no standalone # emulator, and kept files the pack never carries. - standalone_set = {str(c) for c in config.get("standalone_cores") or []} + standalone_set = platform_standalone_set(config) resolved = resolve_platform_cores(config, profiles, target_cores) @@ -449,9 +454,11 @@ def generate_platform_truth( continue cores_profiled.add(emu_name) - filtered = _archives_not_members(_system_dir_files( - profile, runs_standalone(emu_name, profile, standalone_set) - )) + filtered = _archives_not_members([ + fe + for view, standalone in platform_layouts(emu_name, profile, standalone_set) + for fe in _system_dir_files(view, standalone) + ]) on_platform = { _map_sys_id(sid) for sid in profile.get("systems", []) diff --git a/scripts/verify.py b/scripts/verify.py index dd6683f9..71b005a5 100644 --- a/scripts/verify.py +++ b/scripts/verify.py @@ -65,7 +65,9 @@ from common import ( resolve_local_file, ProfileSelectionError, resolve_platform_cores, + platform_layouts, runs_standalone, + standalone_set as platform_standalone_set, sanitize_pack_path, select_emulator_profiles, ) @@ -483,7 +485,7 @@ def find_undeclared_files( ) relevant = resolve_platform_cores(config, profiles, target_cores=target_cores) - standalone_set = set(str(c) for c in config.get("standalone_cores", [])) + standalone_set = platform_standalone_set(config) undeclared = [] seen_files: set[tuple] = set() # Key -> the entry reported for it. A later profile reading the same @@ -503,166 +505,166 @@ def find_undeclared_files( if profile.get("bios_mode") == "agnostic": continue - is_standalone = runs_standalone(emu_name, profile, standalone_set) + for profile, is_standalone in platform_layouts(emu_name, profile, standalone_set): - for f in expand_directory_entries( - profile.get("files", []), db, is_standalone - ): - fname = f.get("name", "") - # One destination rule for the dedup key and for the entry: a - # standalone build without standalone_path falls back on path. - # The key used to skip that fallback, so clk's Acorn/basic.rom and - # Electron/basic.rom both keyed as "basic.rom" under Batocera's - # standalone clk and the second was dropped. - if is_standalone: - dest = f.get("standalone_path") or f.get("path") or fname - else: - dest = f.get("path") or fname - raw_regions = f.get("region") or [] - region_key = tuple( - str(value) for value in ( - raw_regions if isinstance(raw_regions, list) else [raw_regions] + for f in expand_directory_entries( + profile.get("files", []), db, is_standalone + ): + fname = f.get("name", "") + # One destination rule for the dedup key and for the entry: a + # standalone build without standalone_path falls back on path. + # The key used to skip that fallback, so clk's Acorn/basic.rom and + # Electron/basic.rom both keyed as "basic.rom" under Batocera's + # standalone clk and the second was dropped. + if is_standalone: + dest = f.get("standalone_path") or f.get("path") or fname + else: + dest = f.get("path") or fname + raw_regions = f.get("region") or [] + region_key = tuple( + str(value) for value in ( + raw_regions if isinstance(raw_regions, list) else [raw_regions] + ) ) - ) - # Same-name requirements at different paths, for different systems, - # or in distinct variant groups are not interchangeable. - seen_key = ( - fname, - f.get("archive"), - dest, - f.get("system"), - f.get("variant_group"), - region_key, - ) - if not fname: - continue - first = reported.get(seen_key) - duplicate = seen_key in seen_files - if duplicate and (first is None or first["profile"] == emu_name): - continue - verdict = _candidate_verdict( - f, fname, is_standalone, include_all, declared_names, dest - ) - if verdict == "settled": - seen_files.add(seen_key) - continue - if verdict == "skip": - continue - - archive = f.get("archive") - seen_files.add(seen_key) - - # Archived files are grouped by archive - if archive: - archive_key = ( - archive, + # Same-name requirements at different paths, for different systems, + # or in distinct variant groups are not interchangeable. + seen_key = ( + fname, + f.get("archive"), + dest, f.get("system"), f.get("variant_group"), region_key, - is_standalone, ) - if archive_key not in archive_entries: - in_repo = _name_in_index( - archive, by_name, by_path_suffix, data_names, - by_name_lower, - ) - archive_entries[archive_key] = { - "profile": emu_name, - "emulator": profile.get("emulator", emu_name), - "systems": list(profile.get("systems", [])), - "system": f.get("system"), - "region": f.get("region"), - "variant_group": f.get("variant_group"), - "name": archive, - "archive": archive, - "path": archive, - "required": False, - "hle_fallback": False, - "category": f.get("category", "bios"), - "in_repo": in_repo, - "note": "", - "checks": [], - "source_ref": None, - "expected": {}, - "archive_file_count": 0, - "archive_required_count": 0, - "members": archive_members(archive, profile.get("files", [])), - } - entry = archive_entries[archive_key] - entry["archive_file_count"] += 1 - if f.get("required", False): - entry["archive_required_count"] += 1 - entry["required"] = True - continue + if not fname: + continue + first = reported.get(seen_key) + duplicate = seen_key in seen_files + if duplicate and (first is None or first["profile"] == emu_name): + continue + verdict = _candidate_verdict( + f, fname, is_standalone, include_all, declared_names, dest + ) + if verdict == "settled": + seen_files.add(seen_key) + continue + if verdict == "skip": + continue - # Resolution: storage flag, then name, then path basename - storage = f.get("storage", "") - if storage in ("release", "large_file"): - in_repo = True - elif shipped_on_mismatch and (f.get("md5") or f.get("sha1")): - # The entry states what its content should be and the builder - # drops a copy that contradicts it, so content decides here - # too. A name match would not do: generic names collide across - # systems, and answering yes on one describes a pack that will - # not contain the file. - _lp, _st = resolve_local_file(f, db, dest_hint=dest) - in_repo = _lp is not None and _st != "hash_mismatch" - else: - in_repo = _name_in_index( - fname, by_name, by_path_suffix, data_names, by_name_lower, - ) - if not in_repo and dest != fname: - path_base = dest.rsplit("/", 1)[-1] - in_repo = _name_in_index( - path_base, by_name, by_path_suffix, data_names, - by_name_lower, + archive = f.get("archive") + seen_files.add(seen_key) + + # Archived files are grouped by archive + if archive: + archive_key = ( + archive, + f.get("system"), + f.get("variant_group"), + region_key, + is_standalone, ) - if not in_repo: - # Hash fallback: the repo may hold the content under a - # different filename (exos21.rom vs exos21.bin). - # - # A copy contradicting the declared hash counts as held - # only where the builder would ship it. Under existence - # the frontend never opens the file, so the pack carries - # it and reports the divergence; under a digest mode the - # frontend would reject it, the builder leaves it out, and - # calling it available here would describe a pack that - # does not contain it. + if archive_key not in archive_entries: + in_repo = _name_in_index( + archive, by_name, by_path_suffix, data_names, + by_name_lower, + ) + archive_entries[archive_key] = { + "profile": emu_name, + "emulator": profile.get("emulator", emu_name), + "systems": list(profile.get("systems", [])), + "system": f.get("system"), + "region": f.get("region"), + "variant_group": f.get("variant_group"), + "name": archive, + "archive": archive, + "path": archive, + "required": False, + "hle_fallback": False, + "category": f.get("category", "bios"), + "in_repo": in_repo, + "note": "", + "checks": [], + "source_ref": None, + "expected": {}, + "archive_file_count": 0, + "archive_required_count": 0, + "members": archive_members(archive, profile.get("files", [])), + } + entry = archive_entries[archive_key] + entry["archive_file_count"] += 1 + if f.get("required", False): + entry["archive_required_count"] += 1 + entry["required"] = True + continue + + # Resolution: storage flag, then name, then path basename + storage = f.get("storage", "") + if storage in ("release", "large_file"): + in_repo = True + elif shipped_on_mismatch and (f.get("md5") or f.get("sha1")): + # The entry states what its content should be and the builder + # drops a copy that contradicts it, so content decides here + # too. A name match would not do: generic names collide across + # systems, and answering yes on one describes a pack that will + # not contain the file. _lp, _st = resolve_local_file(f, db, dest_hint=dest) - in_repo = _st != "not_found" and _lp is not None - if in_repo and _st == "hash_mismatch" and shipped_on_mismatch: - in_repo = False + in_repo = _lp is not None and _st != "hash_mismatch" + else: + in_repo = _name_in_index( + fname, by_name, by_path_suffix, data_names, by_name_lower, + ) + if not in_repo and dest != fname: + path_base = dest.rsplit("/", 1)[-1] + in_repo = _name_in_index( + path_base, by_name, by_path_suffix, data_names, + by_name_lower, + ) + if not in_repo: + # Hash fallback: the repo may hold the content under a + # different filename (exos21.rom vs exos21.bin). + # + # A copy contradicting the declared hash counts as held + # only where the builder would ship it. Under existence + # the frontend never opens the file, so the pack carries + # it and reports the divergence; under a digest mode the + # frontend would reject it, the builder leaves it out, and + # calling it available here would describe a pack that + # does not contain it. + _lp, _st = resolve_local_file(f, db, dest_hint=dest) + in_repo = _st != "not_found" and _lp is not None + if in_repo and _st == "hash_mismatch" and shipped_on_mismatch: + in_repo = False - checks = _parse_validation(f.get("validation")) - entry = { - "profile": emu_name, - "emulator": profile.get("emulator", emu_name), - "systems": list(profile.get("systems", [])), - "system": f.get("system"), - "region": f.get("region"), - "variant_group": f.get("variant_group"), - "name": fname, - "path": dest, - "required": f.get("required", False), - "hle_fallback": f.get("hle_fallback", False), - "bundled": f.get("bundled", False), - "category": f.get("category", "bios"), - "in_repo": in_repo, - "note": f.get("note", ""), - "checks": sorted(checks) if checks else [], - "source_ref": f.get("source_ref"), - "expected": _build_expected(f, checks), - **_identity_of(f), - "sha1": f.get("sha1"), - "md5": f.get("md5"), - } - if f.get("contents"): - entry["contents"] = f["contents"] - if duplicate: - first.setdefault("other_claims", []).append(entry) - continue - reported[seen_key] = entry - undeclared.append(entry) + checks = _parse_validation(f.get("validation")) + entry = { + "profile": emu_name, + "emulator": profile.get("emulator", emu_name), + "systems": list(profile.get("systems", [])), + "system": f.get("system"), + "region": f.get("region"), + "variant_group": f.get("variant_group"), + "name": fname, + "path": dest, + "required": f.get("required", False), + "hle_fallback": f.get("hle_fallback", False), + "bundled": f.get("bundled", False), + "category": f.get("category", "bios"), + "in_repo": in_repo, + "note": f.get("note", ""), + "checks": sorted(checks) if checks else [], + "source_ref": f.get("source_ref"), + "expected": _build_expected(f, checks), + **_identity_of(f), + "sha1": f.get("sha1"), + "md5": f.get("md5"), + } + if f.get("contents"): + entry["contents"] = f["contents"] + if duplicate: + first.setdefault("other_claims", []).append(entry) + continue + reported[seen_key] = entry + undeclared.append(entry) # Append grouped archive entries for entry in sorted(archive_entries.values(), key=lambda e: e["name"]): @@ -731,7 +733,7 @@ def find_exclusion_notes( # Count standalone-only files -but only report as excluded if the # platform does NOT use this emulator in standalone mode - standalone_set = set(str(c) for c in config.get("standalone_cores", [])) + standalone_set = platform_standalone_set(config) is_standalone = runs_standalone(emu_name, profile, standalone_set) if not is_standalone: standalone_files = [ diff --git a/tests/test_batocera_cores.py b/tests/test_batocera_cores.py index 69261376..118604d1 100644 --- a/tests/test_batocera_cores.py +++ b/tests/test_batocera_cores.py @@ -66,5 +66,61 @@ class TheTargetsNameTheStandaloneEmulator(unittest.TestCase): self.assertEqual(mapping["BR2_PACKAGE_LIBRETRO_DOSBOX_STAGING"], ["dosbox_staging"]) + +class AnEmulatorLaunchedBothWaysGetsBothLayouts(unittest.TestCase): + """es_systems.yml offers libretro:dolphin and standalone dolphin for + GameCube. Filed as standalone only, the pack gave libretro Dolphin no + dolphin-emu/Sys/GC/ files: no IPL, no DSP ROM.""" + + def test_the_scraper_names_dual_mode_emulators(self): + from scripts.scraper.batocera_scraper import es_systems_emulators, es_systems_libretro + + text = ES_SYSTEMS + ( + "gamecube:\n emulators:\n libretro:\n dolphin: { requireAnyOf: [X] }\n" + " dolphin:\n dolphin: { requireAnyOf: [Y] }\n" + ) + _cores, standalone = es_systems_emulators(text) + self.assertEqual(standalone & es_systems_libretro(text), {"dolphin"}) + + def test_the_layouts_are_both(self): + from common import DUAL_MODE_PREFIX, platform_layouts + + profile = {"type": "standalone + libretro", "cores": ["dolphin"], "files": []} + both = platform_layouts("dolphin", profile, {"dolphin", DUAL_MODE_PREFIX + "dolphin"}) + self.assertEqual([standalone for _view, standalone in both], [False, True]) + only = platform_layouts("dolphin", profile, {"dolphin"}) + self.assertEqual([standalone for _view, standalone in only], [True]) + + def test_the_batocera_manifest_carries_both(self): + import json + + manifest = json.loads((REPO_ROOT / "install" / "batocera.json").read_text()) + dests = {entry["dest"] for entry in manifest["files"]} + self.assertIn("dolphin-emu/Sys/GC/USA/IPL.bin", dests) + self.assertIn("GC/USA/IPL.bin", dests) + + def test_retrobat_and_recalbox_name_theirs(self): + from scripts.scraper.recalbox_scraper import system_ini_cores, system_ini_libretro + from scripts.scraper.retrobat_scraper import es_systems_dual + + cfg = ("" + "dolphin" + "") + self.assertEqual(es_systems_dual(cfg), {"dolphin"}) + ini = "[core.0]\nemulator = libretro\ncore = dolphin\n[core.1]\nemulator = dolphin\ncore = dolphin\n" + _cores, standalone = system_ini_cores(ini) + self.assertEqual(system_ini_libretro(ini) & standalone, {"dolphin"}) + + def test_upstream_yaml_is_read_as_its_code_reads_it(self): + """configgen-defaults.yml names rtcw's core twice; the strict loader + stopped the rescrape, Batocera's PyYAML keeps the last value.""" + from safeparse import yaml_load_upstream + + self.assertEqual( + yaml_load_upstream("rtcw:\n core: iortcw\n core: taradino\n"), + {"rtcw": {"core": "taradino"}}, + ) + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_scraper_contract.py b/tests/test_scraper_contract.py index 043d3b08..4b1dec35 100644 --- a/tests/test_scraper_contract.py +++ b/tests/test_scraper_contract.py @@ -203,7 +203,7 @@ class RecalboxReadsItsSystemsInBatches(unittest.TestCase): scraper = recalbox_scraper.Scraper.__new__(recalbox_scraper.Scraper) scraper.tag = "10.1.1" with mock.patch.object(recalbox_scraper, "fetch_bytes", fetch): - cores, _standalone = scraper._fetch_cores() + cores, _standalone, _dual = scraper._fetch_cores() self.assertEqual([len(q) for q in queries], [50, 50, 20]) self.assertEqual(len(cores), 120)