fix: lay out both builds of a dual-mode emulator

Batocera, RetroBat, Recalbox and ROCKNIX offer some emulators both as a
libretro core and standalone. Filed as standalone only, the libretro
layout was dropped: libretro Dolphin had no dolphin-emu/Sys/GC files.
Platforms now name dual-mode emulators and each gets both layouts.
This commit is contained in:
Abdessamad Derraz committed 2026-10-18 13:19:53 +02:00
1 parent 2b535525ef
commit 3c856bbc10
23 files changed
+1367 -478

No files matched your search

+134 -4
View File
@@ -241,10 +241,10 @@
} }
} }
], ],
"total_files": 12899, "total_files": 12907,
"total_size": 9279360834, "total_size": 9288027834,
"pack_files": 12901, "pack_files": 12909,
"pack_size": 9279360834, "pack_size": 9288027834,
"total_omitted": 395, "total_omitted": 395,
"omitted_files": [ "omitted_files": [
{ {
@@ -128447,6 +128447,62 @@
"storage": "release", "storage": "release",
"release_asset": "demo00.pk4" "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", "dest": "GC/dsp_rom.bin",
"sha1": "3c6cc6e04fdd0b2a392d7a6ed769455444846be7", "sha1": "3c6cc6e04fdd0b2a392d7a6ed769455444846be7",
@@ -140832,6 +140888,80 @@
"mame" "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", "dest": "Triforce/segaboot.gcm",
"sha1": "7297a11be143011ceb75a0720f96855003415a5d", "sha1": "7297a11be143011ceb75a0720f96855003415a5d",
+124 -4
View File
@@ -239,10 +239,10 @@
} }
} }
], ],
"total_files": 3418, "total_files": 3426,
"total_size": 4183759111, "total_size": 4192426111,
"pack_files": 3420, "pack_files": 3428,
"pack_size": 4183759111, "pack_size": 4192426111,
"total_omitted": 9, "total_omitted": 9,
"omitted_files": [ "omitted_files": [
{ {
@@ -33254,6 +33254,68 @@
"corsixth" "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", "dest": "GC/dsp_rom.bin",
"sha1": "3c6cc6e04fdd0b2a392d7a6ed769455444846be7", "sha1": "3c6cc6e04fdd0b2a392d7a6ed769455444846be7",
@@ -38601,6 +38663,64 @@
"nintendo-ds" "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", "dest": "GC/USA/IPL.bin",
"sha1": "ef9194ab4804aa0aa8540d846caf291b28331165", "sha1": "ef9194ab4804aa0aa8540d846caf291b28331165",
+157 -4
View File
@@ -245,10 +245,10 @@
} }
} }
], ],
"total_files": 7740, "total_files": 7751,
"total_size": 7988832663, "total_size": 7997666672,
"pack_files": 7742, "pack_files": 7753,
"pack_size": 7988832663, "pack_size": 7997666672,
"total_omitted": 135, "total_omitted": 135,
"omitted_files": [ "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", "dest": "Config/Games.xml",
"sha1": "6871a47de90ca67f9ff8e4e280416851a8164563", "sha1": "6871a47de90ca67f9ff8e4e280416851a8164563",
@@ -74985,6 +75015,116 @@
"storage": "release", "storage": "release",
"release_asset": "demo00.pk4" "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", "dest": "GC/USA/IPL.bin",
"sha1": "ef9194ab4804aa0aa8540d846caf291b28331165", "sha1": "ef9194ab4804aa0aa8540d846caf291b28331165",
@@ -87723,6 +87863,19 @@
"mame2016" "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", "dest": "GBA/gba_bios.bin",
"sha1": "300c20df6731a33952ded8c436f7f186d25d3492", "sha1": "300c20df6731a33952ded8c436f7f186d25d3492",
+258 -73
View File
@@ -17,10 +17,10 @@
} }
], ],
"standalone_copies": [], "standalone_copies": [],
"total_files": 6115, "total_files": 6132,
"total_size": 7848090128, "total_size": 7913710008,
"pack_files": 6117, "pack_files": 6134,
"pack_size": 7848090128, "pack_size": 7913710008,
"total_omitted": 49, "total_omitted": 49,
"omitted_files": [ "omitted_files": [
{ {
@@ -1964,6 +1964,7 @@
"size": 4194304, "size": 4194304,
"repo_path": "bios/Sony/PlayStation 2/ps2-0230a-20080220.bin", "repo_path": "bios/Sony/PlayStation 2/ps2-0230a-20080220.bin",
"cores": [ "cores": [
"armsx2",
"yaps2" "yaps2"
] ]
} }
@@ -2879,6 +2880,46 @@
"cannonball" "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", "dest": "resources/sharedFonts/CafeCn.ttf",
"sha1": "2d95227b9b3eaf1c74d9293ecd0d190ec7ffbbe3", "sha1": "2d95227b9b3eaf1c74d9293ecd0d190ec7ffbbe3",
@@ -47546,6 +47587,36 @@
"spmp8000emu" "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", "dest": "Config/Games.xml",
"sha1": "6871a47de90ca67f9ff8e4e280416851a8164563", "sha1": "6871a47de90ca67f9ff8e4e280416851a8164563",
@@ -50323,6 +50394,90 @@
"cdi2015" "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", "dest": "keys.txt",
"sha1": "9edc52be45201ec99f6e4ceb5dc6abfc633c4eae", "sha1": "9edc52be45201ec99f6e4ceb5dc6abfc633c4eae",
@@ -50464,6 +50619,72 @@
"microsoft-xbox" "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", "dest": "GC/dsp_rom.bin",
"sha1": "3c6cc6e04fdd0b2a392d7a6ed769455444846be7", "sha1": "3c6cc6e04fdd0b2a392d7a6ed769455444846be7",
@@ -53367,58 +53588,6 @@
"hypseus-singe" "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", "dest": "jazz2/Source/AnimsSw.j2a",
"sha1": "133392a2c638d17796c6f510cf94ec391cc4c407", "sha1": "133392a2c638d17796c6f510cf94ec391cc4c407",
@@ -61993,7 +62162,7 @@
] ]
}, },
{ {
"dest": "eeprom.bin", "dest": "xbox_eeprom.bin",
"sha1": "ff3e7eaf715fe5612e46fc984d686ed3b115baef", "sha1": "ff3e7eaf715fe5612e46fc984d686ed3b115baef",
"sha256": "e652203e6d91140729c29588b18ff8838896cdeb9a3100fbf8d20763af27c9d2", "sha256": "e652203e6d91140729c29588b18ff8838896cdeb9a3100fbf8d20763af27c9d2",
"size": 256, "size": 256,
@@ -62031,6 +62200,19 @@
"microsoft-xbox" "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", "dest": "cdb/cdb_rom.bin",
"sha1": "b3c20fbe57cd2eb595e9edac86817e5948dccae4", "sha1": "b3c20fbe57cd2eb595e9edac86817e5948dccae4",
@@ -62692,19 +62874,6 @@
"microsoft-xbox" "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", "dest": "dolphin-emu/Sys/GC/USA/IPL.bin",
"sha1": "ef9194ab4804aa0aa8540d846caf291b28331165", "sha1": "ef9194ab4804aa0aa8540d846caf291b28331165",
@@ -62712,6 +62881,7 @@
"size": 2097152, "size": 2097152,
"repo_path": "bios/Nintendo/GameCube/GC/USA/IPL.bin", "repo_path": "bios/Nintendo/GameCube/GC/USA/IPL.bin",
"cores": [ "cores": [
"dolphin",
"ishiiruka" "ishiiruka"
], ],
"systems": [ "systems": [
@@ -62721,7 +62891,7 @@
"north-america" "north-america"
], ],
"region_groups": [ "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, "size": 2097152,
"repo_path": "bios/Nintendo/GameCube/GC/EUR/IPL.bin", "repo_path": "bios/Nintendo/GameCube/GC/EUR/IPL.bin",
"cores": [ "cores": [
"dolphin",
"ishiiruka" "ishiiruka"
], ],
"systems": [ "systems": [
@@ -62741,7 +62912,7 @@
"oceania" "oceania"
], ],
"region_groups": [ "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, "size": 2097152,
"repo_path": "bios/Nintendo/GameCube/GC/JAP/IPL.bin", "repo_path": "bios/Nintendo/GameCube/GC/JAP/IPL.bin",
"cores": [ "cores": [
"dolphin",
"ishiiruka" "ishiiruka"
], ],
"systems": [ "systems": [
@@ -62760,7 +62932,20 @@
"japan" "japan"
], ],
"region_groups": [ "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"
] ]
}, },
{ {
+14
View File
@@ -5803,3 +5803,17 @@ standalone_cores:
- xvic - xvic
- ymir - ymir
- yquake2 - yquake2
dual_mode_cores:
- applewin
- dolphin
- easyrpg
- flycast
- hatari
- mame
- melonds
- pcsx2
- play
- ppsspp
- scummvm
- stella
- tic80
+10
View File
@@ -262,6 +262,16 @@ standalone_cores:
- xroar - xroar
- xscpu64 - xscpu64
- xvic - xvic
dual_mode_cores:
- amiberry
- dolphin
- hatari
- mupen64plus
- pcsx2
- pcsx_rearmed
- ppsspp
- rb5000
- scummvm
systems: systems:
commodore-amiga: commodore-amiga:
files: files:
+21
View File
@@ -421,6 +421,27 @@ standalone_cores:
- zesarux - zesarux
- zinc - zinc
- zquestclassic - 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: systems:
3do: 3do:
files: files:
+64
View File
@@ -134,6 +134,70 @@ standalone_cores:
- xemu-sa - xemu-sa
- yabasanshiro - yabasanshiro
- yabasanshiro-sa - 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: systems:
microsoft-xbox: microsoft-xbox:
files: files:
+8
View File
@@ -45,6 +45,14 @@
}, },
"uniqueItems": true "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": { "homepage": {
"type": "string", "type": "string",
"format": "uri", "format": "uri",
+39
View File
@@ -1861,6 +1861,7 @@ def group_identical_platforms(
emulators = [ emulators = [
cores if isinstance(cores, str) else sorted(map(str, cores or [])), cores if isinstance(cores, str) else sorted(map(str, cores or [])),
sorted(map(str, config.get("standalone_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 # One archive cannot serve a filesystem that folds case and one
# that does not: Lakka keeps bios.col beside BIOS.col. # that does not: Lakka keeps bios.col beside BIOS.col.
bool(config.get("case_insensitive_fs")), 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 # What identifies a profile entry's content, carried unchanged wherever the
# entry travels (cross-reference report, core extras, second-pass copies). # 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 # Each copy once kept its own subset: np2kai's bios.rom lost min_size and
+11 -4
View File
@@ -38,7 +38,8 @@ from common import (
require_platform, require_platform,
require_yaml, require_yaml,
resolve_local_file, resolve_local_file,
runs_standalone, platform_layouts,
standalone_set,
select_emulator_profiles, select_emulator_profiles,
) )
from validation import declaration_settles, outside_gap_scope 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 # The build a platform runs decides which entries it reads, as in
# find_undeclared_files: without a platform, the libretro build. # 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 = [] gaps = []
covered = [] covered = []
unsourceable_list: list[dict] = [] unsourceable_list: list[dict] = []
@@ -516,7 +520,7 @@ def _cross_reference_profile(
for f in emu_files: for f in emu_files:
versions.setdefault(slot_key(f), []).append(f) versions.setdefault(slot_key(f), []).append(f)
for f in emu_files: for f, is_standalone in layout_files:
fname = f.get("name", "") fname = f.get("name", "")
# The destination the running build reads, as find_undeclared_files # The destination the running build reads, as find_undeclared_files
# takes it: Batocera's standalone Cemu reads keys.txt at # takes it: Batocera's standalone Cemu reads keys.txt at
@@ -527,6 +531,9 @@ def _cross_reference_profile(
else: else:
effective_path = f.get("path") or fname effective_path = f.get("path") or fname
seen_key = slot_key(f) 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: if not fname or seen_key in seen_files:
continue continue
@@ -820,7 +827,7 @@ def main() -> None:
config = load_platform_config(args.platform, args.platforms_dir) config = load_platform_config(args.platform, args.platforms_dir)
relevant = resolve_platform_cores(config, profiles, target_cores=target_cores) relevant = resolve_platform_cores(config, profiles, target_cores=target_cores)
profiles = {k: v for k, v in profiles.items() if k in relevant} 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 not profiles:
if args.emulator: if args.emulator:
+158 -157
View File
@@ -15,7 +15,8 @@ from common import load_emulator_profiles
from common import resolution_is_hash_exact from common import resolution_is_hash_exact
from common import resolve_local_file from common import resolve_local_file
from common import resolve_platform_cores 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 sanitize_pack_path
from common import size_fits from common import size_fits
from validation import read_from_system_dir from validation import read_from_system_dir
@@ -290,96 +291,96 @@ def _agnostic_scan_extras(
agnostic_votes.get(directory + "/", 0) + 1 agnostic_votes.get(directory + "/", 0) + 1
) )
standalone = runs_standalone(emu_name, profile, standalone_set or set()) for profile, standalone in platform_layouts(emu_name, profile, standalone_set or set()):
for f in profile.get("files", []): for f in profile.get("files", []):
if not is_profile_agnostic and not f.get("agnostic"): if not is_profile_agnostic and not f.get("agnostic"):
continue continue
fname = f.get("name", "") fname = f.get("name", "")
if not fname: if not fname:
continue continue
# A file of the build the platform does not run seeds nothing: # 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 # armsx2's proverb.zip is fetched by the Android app into its own
# arcade/ folder, and the libretro packs carried it at system/. # arcade/ folder, and the libretro packs carried it at system/.
mode = f.get("mode") mode = f.get("mode")
if (mode == "standalone" and not standalone) or ( if (mode == "standalone" and not standalone) or (
mode == "libretro" and standalone mode == "libretro" and standalone
): ):
continue continue
# An agnostic BIOS mode says the BIOS filename is free, not that # An agnostic BIOS mode says the BIOS filename is free, not that
# every file the emulator loads is. A free filename still has a # every file the emulator loads is. A free filename still has a
# known shape, and that shape is what identifies a candidate: an # known shape, and that shape is what identifies a candidate: an
# entry declaring no size takes the whole directory, which is how # entry declaring no size takes the whole directory, which is how
# the flag icons of an Android package walked into a PS2 pack. # the flag icons of an Android package walked into a PS2 pack.
if f.get("category", "bios") != "bios": if f.get("category", "bios") != "bios":
continue continue
if not (f.get("size") or f.get("min_size") or f.get("max_size")): if not (f.get("size") or f.get("min_size") or f.get("max_size")):
continue continue
# The free name is the BIOS directory's: a shader, a sound or a # 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 # font the emulator reads from its resources tree has a fixed
# name and must not seed a scan (yaps2 flattened 189 of them to # name and must not seed a scan (yaps2 flattened 189 of them to
# the root of the RetroArch pack). # the root of the RetroArch pack).
bios_dir = profile.get("bios_directory") bios_dir = profile.get("bios_directory")
fpath = f.get("path") or "" fpath = f.get("path") or ""
if bios_dir and "/" in fpath and not fpath.startswith(bios_dir): if bios_dir and "/" in fpath and not fpath.startswith(bios_dir):
continue continue
path_prefix = resolved_dirs.get(id(f), "") path_prefix = resolved_dirs.get(id(f), "")
if not path_prefix: if not path_prefix:
continue continue
# A name matching one file identifies it. A name matching several # A name matching one file identifies it. A name matching several
# identifies nothing on its own, so the profile's other files have # identifies nothing on its own, so the profile's other files have
# to agree on the directory before the scan walks it. # to agree on the directory before the scan walks it.
ambiguous = id(f) in named_only and len(by_name.get(fname, [])) > 1 ambiguous = id(f) in named_only and len(by_name.get(fname, [])) > 1
if ambiguous and agnostic_votes.get(path_prefix, 0) < 2: if ambiguous and agnostic_votes.get(path_prefix, 0) < 2:
continue continue
# Scan DB for all files under this prefix matching size # Scan DB for all files under this prefix matching size
for sha1, entry in files_db.items(): for sha1, entry in files_db.items():
path = entry.get("path", "") path = entry.get("path", "")
if not path.startswith(path_prefix): if not path.startswith(path_prefix):
continue continue
if not size_fits(f, entry.get("size", 0)): if not size_fits(f, entry.get("size", 0)):
continue continue
scan_name = entry.get("name", "") scan_name = entry.get("name", "")
if not scan_name: if not scan_name:
continue continue
# Into the directory the seed entry names, not the root: # Into the directory the seed entry names, not the root:
# yaps2 reads its BIOS from pcsx2/bios/. # yaps2 reads its BIOS from pcsx2/bios/.
seed = f.get("path") or "" seed = f.get("path") or ""
if standalone: if standalone:
seed = f.get("standalone_path") or seed seed = f.get("standalone_path") or seed
folder = seed.rsplit("/", 1)[0] if "/" in seed else "" folder = seed.rsplit("/", 1)[0] if "/" in seed else ""
dest = f"{folder}/{scan_name}" if folder else scan_name dest = f"{folder}/{scan_name}" if folder else scan_name
full_dest = f"{extras_prefix}/{dest}" if extras_prefix else dest 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): if full_dest in seen_dests and (taken is None or full_dest not in taken):
continue continue
extra = { extra = {
"name": scan_name, "name": scan_name,
"destination": dest, "destination": dest,
# The scan already holds the entry it selected, so it # The scan already holds the entry it selected, so it
# names it by content. Emitting the filename alone # names it by content. Emitting the filename alone
# sent the packing step back to a by-name lookup, and # sent the packing step back to a by-name lookup, and
# three PS2 resources came back from another # three PS2 resources came back from another
# emulator's copy of the same filename. # emulator's copy of the same filename.
"sha1": sha1, "sha1": sha1,
"required": False, "required": False,
"hle_fallback": False, "hle_fallback": False,
"source_emulator": profile.get("emulator", emu_name), "source_emulator": profile.get("emulator", emu_name),
"source_profile": emu_name, "source_profile": emu_name,
"source_system": f.get("system"), "source_system": f.get("system"),
"source_systems": list(profile.get("systems", [])), "source_systems": list(profile.get("systems", [])),
"region": f.get("region"), "region": f.get("region"),
"variant_group": f.get("variant_group"), "variant_group": f.get("variant_group"),
"agnostic_scan": True, "agnostic_scan": True,
} }
extra["claim_order"] = (4, 0, emu_name) extra["claim_order"] = (4, 0, emu_name)
if not _take(extra, full_dest, seen_dests, taken): if not _take(extra, full_dest, seen_dests, taken):
continue continue
extras.append(extra) extras.append(extra)
extras.extend(_companion_extras( extras.extend(_companion_extras(
_companions(path, scan_name, _companion_extensions(profile, fname), files_db), _companions(path, scan_name, _companion_extensions(profile, fname), files_db),
folder, extras_prefix, seen_dests, profile, emu_name, f, taken, folder, extras_prefix, seen_dests, profile, emu_name, f, taken,
)) ))
return extras return extras
@@ -701,7 +702,7 @@ def _collect_emulator_extras(
return extras return extras
relevant = resolve_platform_cores(config, profiles, target_cores=target_cores) 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_name = db.get("indexes", {}).get("by_name", {})
by_path_suffix = db.get("indexes", {}).get("by_path_suffix", {}) by_path_suffix = db.get("indexes", {}).get("by_path_suffix", {})
@@ -716,73 +717,73 @@ def _collect_emulator_extras(
continue continue
if emu_name not in relevant: if emu_name not in relevant:
continue 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 profile.get("files", []): for f in profile.get("files", []):
fname = f.get("name", "") fname = f.get("name", "")
if not fname: if not fname:
continue continue
# Only duplicate files already covered at another destination. # Only duplicate files already covered at another destination.
# An unsourceable entry is a file no collection holds: the name # An unsourceable entry is a file no collection holds: the name
# covered elsewhere belongs to another program's file. # covered elsewhere belongs to another program's file.
if fname not in covered_names or f.get("unsourceable"): if fname not in covered_names or f.get("unsourceable"):
continue continue
# A member of an archive is read inside it, never as a loose # A member of an archive is read inside it, never as a loose
# file; the archive itself is placed by the passes above. # file; the archive itself is placed by the passes above.
if f.get("archive"): if f.get("archive"):
continue continue
# No path: the core reads it from nowhere a pack reaches # No path: the core reads it from nowhere a pack reaches
# (Dolphin's codehandler.bin ships in its Sys tree). # (Dolphin's codehandler.bin ships in its Sys tree).
if "path" in f and f["path"] is None: if "path" in f and f["path"] is None:
continue continue
file_mode = f.get("mode") file_mode = f.get("mode")
if file_mode == "standalone" and not is_standalone: if file_mode == "standalone" and not is_standalone:
continue continue
if file_mode == "libretro" and is_standalone: if file_mode == "libretro" and is_standalone:
continue continue
if not read_from_system_dir(f, is_standalone): if not read_from_system_dir(f, is_standalone):
continue continue
if is_standalone: if is_standalone:
raw = f.get("standalone_path") or f.get("path") or fname raw = f.get("standalone_path") or f.get("path") or fname
else: else:
raw = f.get("path") or fname raw = f.get("path") or fname
dest = f"{raw}{fname}" if raw.endswith("/") else raw dest = f"{raw}{fname}" if raw.endswith("/") else raw
# A bare name is a destination too: the core reads it at the root # 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 # of the directory core files go to, which the platform may not
# declare (Recalbox has its System Cards under pcenginecd/ only, # declare (Recalbox has its System Cards under pcenginecd/ only,
# geargrafx reads them from the root). # geargrafx reads them from the root).
full_dest = f"{extras_prefix}/{dest}" if extras_prefix else dest full_dest = f"{extras_prefix}/{dest}" if extras_prefix else dest
if full_dest in seen_dests and full_dest not in taken: if full_dest in seen_dests and full_dest not in taken:
continue continue
# Check file exists in repo or data dirs # Check file exists in repo or data dirs
if not ( if not (
by_name.get(fname) by_name.get(fname)
or by_name.get(dest.rsplit("/", 1)[-1]) or by_name.get(dest.rsplit("/", 1)[-1])
or by_path_suffix.get(dest) or by_path_suffix.get(dest)
): ):
continue continue
extra = { extra = {
"name": fname, "name": fname,
"destination": dest, "destination": dest,
"required": f.get("required", False), "required": f.get("required", False),
"hle_fallback": f.get("hle_fallback", False), "hle_fallback": f.get("hle_fallback", False),
"source_emulator": profile.get("emulator", emu_name), "source_emulator": profile.get("emulator", emu_name),
"source_profile": emu_name, "source_profile": emu_name,
"source_system": f.get("system"), "source_system": f.get("system"),
"source_systems": list(profile.get("systems", [])), "source_systems": list(profile.get("systems", [])),
"region": f.get("region"), "region": f.get("region"),
"variant_group": f.get("variant_group"), "variant_group": f.get("variant_group"),
} }
# The same identity the first pass carries: without it the copy # The same identity the first pass carries: without it the copy
# resolves on its name alone and a same-named file of another # resolves on its name alone and a same-named file of another
# game fills the slot the profile marks as not held. # game fills the slot the profile marks as not held.
for identity in PROFILE_IDENTITY_FIELDS: for identity in PROFILE_IDENTITY_FIELDS:
if f.get(identity) not in (None, "", []): if f.get(identity) not in (None, "", []):
extra[identity] = f[identity] extra[identity] = f[identity]
if f.get("contents"): if f.get("contents"):
extra["contents"] = f["contents"] extra["contents"] = f["contents"]
extra["claim_order"] = (2, 0, emu_name) extra["claim_order"] = (2, 0, emu_name)
if _take(extra, full_dest, seen_dests, taken): if _take(extra, full_dest, seen_dests, taken):
extras.append(extra) extras.append(extra)
extras.extend( extras.extend(
_archive_prefix_extras( _archive_prefix_extras(
+12
View File
@@ -97,3 +97,15 @@ def yaml_load(stream: str | TextIO) -> Any:
if yaml is None: if yaml is None:
return require_yaml() # exits with the install hint return require_yaml() # exits with the install hint
return yaml.load(stream, Loader=_YAML_LOADER) 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)
+30 -10
View File
@@ -18,6 +18,7 @@ from pathlib import Path
from common import yaml_load from common import yaml_load
from safeparse import yaml_load_upstream
from .base_scraper import ( from .base_scraper import (
BaseScraper, 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 reading it alone left DOSBox Staging, melonDS DS and nine other built
cores outside the pack. cores outside the pack.
""" """
data = yaml_load(text) data = yaml_load_upstream(text)
if not isinstance(data, dict): if not isinstance(data, dict):
raise ValueError("es_systems.yml is not a mapping") raise ValueError("es_systems.yml is not a mapping")
cores: set[str] = set() cores: set[str] = set()
@@ -126,6 +127,20 @@ def es_systems_emulators(text: str) -> tuple[set[str], set[str]]:
return cores, standalone 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 = { SYSTEM_SLUG_MAP = {
"advision": "entex-advision", "advision": "entex-advision",
"amiga500": "commodore-amiga", "amiga500": "commodore-amiga",
@@ -253,11 +268,12 @@ class Scraper(BaseScraper):
self.tag = fetch_stable_tag() self.tag = fetch_stable_tag()
super().__init__(url=SOURCE_URL.format(tag=self.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. """Extract core names and standalone cores from configgen-defaults.yml.
Returns (all_cores, standalone_cores) where standalone_cores are Returns (all_cores, standalone_cores, dual_mode_cores): standalone
those with emulator != "libretro". 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) url = CONFIGGEN_DEFAULTS_URL.format(tag=self.tag)
try: try:
@@ -271,10 +287,10 @@ class Scraper(BaseScraper):
raise ConnectionError( raise ConnectionError(
f"Failed to fetch {url}: {e}" f"Failed to fetch {url}: {e}"
) from e ) from e
data = yaml_load(raw) data = yaml_load_upstream(raw)
cores, standalone = es_systems_emulators( es_systems = self._fetch_text(ES_SYSTEMS_URL.format(tag=self.tag))
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: if not cores:
raise RuntimeError("es_systems.yml names no emulator") raise RuntimeError("es_systems.yml names no emulator")
for system, cfg in data.items(): for system, cfg in data.items():
@@ -289,7 +305,8 @@ class Scraper(BaseScraper):
if core and core != emulator: if core and core != emulator:
standalone.add(core) standalone.add(core)
dropped = _not_cores() 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: def _fetch_text(self, url: str) -> str:
req = urllib.request.Request(url, headers={"User-Agent": "retrobios-scraper/1.0"}) 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)) systems[req.system]["files"].append(requirement_entry(req))
cores, standalone = self._fetch_cores() cores, standalone, dual = self._fetch_cores()
result = { result = {
"platform": "Batocera", "platform": "Batocera",
"version": self.tag.removeprefix("batocera-"), "version": self.tag.removeprefix("batocera-"),
@@ -463,6 +480,9 @@ class Scraper(BaseScraper):
} }
if standalone: if standalone:
result["standalone_cores"] = standalone result["standalone_cores"] = standalone
if dual:
# Launched both ways: each layout gets its files.
result["dual_mode_cores"] = dual
return result return result
+21 -4
View File
@@ -153,6 +153,18 @@ def _core_names(values: dict[str, str]) -> tuple[set[str], set[str]]:
return names, set(names) 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]]: def system_ini_cores(text: str) -> tuple[set[str], set[str]]:
"""The cores one Recalbox system.ini offers, and those run standalone. """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])}") raise RuntimeError(f"no system.ini at {self.tag} for {', '.join(missing[:5])}")
return [texts[path] for path in paths] return [texts[path] for path in paths]
def _fetch_cores(self) -> tuple[list[str], list[str]]: def _fetch_cores(self) -> tuple[list[str], list[str], list[str]]:
"""Every core Recalbox's systems offer, and those it runs standalone. """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 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. 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}") raise RuntimeError(f"no Recalbox system found at {self.tag}")
cores: set[str] = set() cores: set[str] = set()
standalone: set[str] = set() standalone: set[str] = set()
libretro: set[str] = set()
for text in self._system_inis(systems): for text in self._system_inis(systems):
found, alone = system_ini_cores(text) found, alone = system_ini_cores(text)
cores |= found cores |= found
standalone |= alone standalone |= alone
libretro |= system_ini_libretro(text)
if not cores: if not cores:
raise RuntimeError(f"no core in Recalbox's system.ini files at {self.tag}") 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]: def fetch_requirements(self) -> list[BiosRequirement]:
"""Parse es_bios.xml and return BIOS requirements.""" """Parse es_bios.xml and return BIOS requirements."""
@@ -350,7 +365,7 @@ class Scraper(BaseScraper):
systems[req.system]["files"].append(requirement_entry(req)) systems[req.system]["files"].append(requirement_entry(req))
cores, standalone = self._fetch_cores() cores, standalone, dual = self._fetch_cores()
return { return {
"platform": "Recalbox", "platform": "Recalbox",
"version": self.tag, "version": self.tag,
@@ -361,6 +376,8 @@ class Scraper(BaseScraper):
"verification_mode": "md5", "verification_mode": "md5",
"cores": cores, "cores": cores,
"standalone_cores": standalone, "standalone_cores": standalone,
# Launched both ways: each layout gets its files.
**({"dual_mode_cores": dual} if dual else {}),
"systems": systems, "systems": systems,
} }
+20 -1
View File
@@ -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]]: def es_systems_cores(text: str) -> tuple[set[str], set[str]]:
"""Every core RetroBat's ES offers, and the emulators it runs standalone. """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"}) req = urllib.request.Request(es_url, headers={"User-Agent": "retrobios-scraper/1.0"})
try: try:
with urllib.request.urlopen(req, timeout=30) as resp: 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: except urllib.error.URLError as e:
raise ConnectionError(f"Failed to fetch {es_url}: {e}") from e raise ConnectionError(f"Failed to fetch {es_url}: {e}") from e
if not cores: if not cores:
@@ -223,6 +240,8 @@ class Scraper(BaseScraper):
"verification_mode": "md5", "verification_mode": "md5",
"cores": sorted(cores), "cores": sorted(cores),
"standalone_cores": sorted(standalone), "standalone_cores": sorted(standalone),
# Launched both ways: each layout gets its files.
**({"dual_mode_cores": sorted(dual)} if dual else {}),
"systems": systems, "systems": systems,
} }
+3
View File
@@ -236,6 +236,9 @@ class Scraper(BaseScraper):
# emulators ROCKNIX packages beside it. # emulators ROCKNIX packages beside it.
"cores": ["retroarch", *standalone], "cores": ["retroarch", *standalone],
"standalone_cores": 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, "systems": systems,
} }
@@ -23,7 +23,7 @@ from datetime import datetime, timezone
import yaml import yaml
from common import yaml_load from safeparse import yaml_load_upstream
from . import BaseTargetScraper from . import BaseTargetScraper
@@ -244,7 +244,7 @@ def _parse_es_systems(text: str) -> dict[str, list[str]]:
<core_name>: {requireAnyOf: [BR2_PACKAGE_FOO]} <core_name>: {requireAnyOf: [BR2_PACKAGE_FOO]}
""" """
try: try:
data = yaml_load(text) data = yaml_load_upstream(text)
except yaml.YAMLError: except yaml.YAMLError:
return {} return {}
+58 -57
View File
@@ -32,7 +32,8 @@ from common import (
resolution_is_hash_exact, resolution_is_hash_exact,
resolve_local_file, resolve_local_file,
resolve_platform_cores, resolve_platform_cores,
runs_standalone, platform_layouts,
standalone_set as platform_standalone_set,
variant_base_name, variant_base_name,
) )
from packresolve import preferred_entries from packresolve import preferred_entries
@@ -202,63 +203,63 @@ def profile_claims(
# carries its scan candidates, never its named entries. # carries its scan candidates, never its named entries.
if profile.get("bios_mode") == "agnostic": if profile.get("bios_mode") == "agnostic":
continue continue
is_standalone = runs_standalone(emu_name, profile, standalone_cores) for profile, is_standalone in platform_layouts(emu_name, profile, standalone_cores):
claims.extend( claims.extend(
_archive_claims( _archive_claims(
emu_name, profile, is_standalone, db, base_dest, placement, emu_name, profile, is_standalone, db, base_dest, placement,
zip_contents, data_dir_registry, 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
) )
if placed is None: )
for entry in profile.get("files") or []:
if not isinstance(entry, dict):
continue continue
full = placed[1] # The same rule as the gap report and the builder: an entry the
# The owner's own copy is the one the builder ships. # pack never carries (read from the save directory, a
local, status = resolve_local_file( # placeholder, a filename-agnostic scan) arbitrates nothing.
{**entry, "source_profile": emu_name}, if outside_gap_scope(entry, is_standalone):
db, continue
zip_contents, # A member of a romset is not a file at the BIOS root: the archive
dest_hint=dest, # holding it occupies the destination (_archive_claims). Claiming
data_dir_registry=data_dir_registry, # the member put FBNeo's msx.zip contents loose at the root.
) if entry.get("archive"):
if status == "name_exact" and _size_the_code_reads(entry, local): continue
status = SIZE_EXACT dest = (
claims.append( (entry.get("standalone_path") or entry.get("path"))
Claim( if is_standalone
origin="profile", else entry.get("path")
destination=full, ) or entry.get("name") or ""
name=entry.get("name", ""), if not dest:
emulator=emu_name, continue
entry=entry, if placement is None:
local_path=local, full = f"{base_dest}/{dest}" if base_dest else dest
status=status, 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 return claims
@@ -524,7 +525,7 @@ def slot_choices(
platform_profiles, platform_profiles,
db, db,
config.get("base_destination", ""), config.get("base_destination", ""),
{str(c) for c in config.get("standalone_cores", [])}, platform_standalone_set(config),
zip_contents, zip_contents,
data_dir_registry, data_dir_registry,
) )
@@ -821,7 +822,7 @@ def scan_platform(
relevant, relevant,
db, db,
config.get("base_destination", ""), config.get("base_destination", ""),
{str(c) for c in config.get("standalone_cores", [])}, platform_standalone_set(config),
build_zip_contents_index(db), build_zip_contents_index(db),
load_data_dir_registry(platforms_dir), load_data_dir_registry(platforms_dir),
) )
+12 -5
View File
@@ -8,7 +8,12 @@ from __future__ import annotations
import sys 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 hashing import parse_md5_list
from validation import filter_files_by_mode, read_from_system_dir 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 # The layout rule verify and the pack builder apply: truth guessed its
# own from the profile type where the platform names no standalone # own from the profile type where the platform names no standalone
# emulator, and kept files the pack never carries. # 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) resolved = resolve_platform_cores(config, profiles, target_cores)
@@ -449,9 +454,11 @@ def generate_platform_truth(
continue continue
cores_profiled.add(emu_name) cores_profiled.add(emu_name)
filtered = _archives_not_members(_system_dir_files( filtered = _archives_not_members([
profile, runs_standalone(emu_name, profile, standalone_set) fe
)) for view, standalone in platform_layouts(emu_name, profile, standalone_set)
for fe in _system_dir_files(view, standalone)
])
on_platform = { on_platform = {
_map_sys_id(sid) for sid in profile.get("systems", []) _map_sys_id(sid) for sid in profile.get("systems", [])
+154 -152
View File
@@ -65,7 +65,9 @@ from common import (
resolve_local_file, resolve_local_file,
ProfileSelectionError, ProfileSelectionError,
resolve_platform_cores, resolve_platform_cores,
platform_layouts,
runs_standalone, runs_standalone,
standalone_set as platform_standalone_set,
sanitize_pack_path, sanitize_pack_path,
select_emulator_profiles, select_emulator_profiles,
) )
@@ -483,7 +485,7 @@ def find_undeclared_files(
) )
relevant = resolve_platform_cores(config, profiles, target_cores=target_cores) 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 = [] undeclared = []
seen_files: set[tuple] = set() seen_files: set[tuple] = set()
# Key -> the entry reported for it. A later profile reading the same # 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": if profile.get("bios_mode") == "agnostic":
continue 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( for f in expand_directory_entries(
profile.get("files", []), db, is_standalone profile.get("files", []), db, is_standalone
): ):
fname = f.get("name", "") fname = f.get("name", "")
# One destination rule for the dedup key and for the entry: a # One destination rule for the dedup key and for the entry: a
# standalone build without standalone_path falls back on path. # standalone build without standalone_path falls back on path.
# The key used to skip that fallback, so clk's Acorn/basic.rom and # 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 # Electron/basic.rom both keyed as "basic.rom" under Batocera's
# standalone clk and the second was dropped. # standalone clk and the second was dropped.
if is_standalone: if is_standalone:
dest = f.get("standalone_path") or f.get("path") or fname dest = f.get("standalone_path") or f.get("path") or fname
else: else:
dest = f.get("path") or fname dest = f.get("path") or fname
raw_regions = f.get("region") or [] raw_regions = f.get("region") or []
region_key = tuple( region_key = tuple(
str(value) for value in ( str(value) for value in (
raw_regions if isinstance(raw_regions, list) else [raw_regions] raw_regions if isinstance(raw_regions, list) else [raw_regions]
)
) )
) # Same-name requirements at different paths, for different systems,
# Same-name requirements at different paths, for different systems, # or in distinct variant groups are not interchangeable.
# or in distinct variant groups are not interchangeable. seen_key = (
seen_key = ( fname,
fname, f.get("archive"),
f.get("archive"), dest,
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,
f.get("system"), f.get("system"),
f.get("variant_group"), f.get("variant_group"),
region_key, region_key,
is_standalone,
) )
if archive_key not in archive_entries: if not fname:
in_repo = _name_in_index( continue
archive, by_name, by_path_suffix, data_names, first = reported.get(seen_key)
by_name_lower, duplicate = seen_key in seen_files
) if duplicate and (first is None or first["profile"] == emu_name):
archive_entries[archive_key] = { continue
"profile": emu_name, verdict = _candidate_verdict(
"emulator": profile.get("emulator", emu_name), f, fname, is_standalone, include_all, declared_names, dest
"systems": list(profile.get("systems", [])), )
"system": f.get("system"), if verdict == "settled":
"region": f.get("region"), seen_files.add(seen_key)
"variant_group": f.get("variant_group"), continue
"name": archive, if verdict == "skip":
"archive": archive, continue
"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 archive = f.get("archive")
storage = f.get("storage", "") seen_files.add(seen_key)
if storage in ("release", "large_file"):
in_repo = True # Archived files are grouped by archive
elif shipped_on_mismatch and (f.get("md5") or f.get("sha1")): if archive:
# The entry states what its content should be and the builder archive_key = (
# drops a copy that contradicts it, so content decides here archive,
# too. A name match would not do: generic names collide across f.get("system"),
# systems, and answering yes on one describes a pack that will f.get("variant_group"),
# not contain the file. region_key,
_lp, _st = resolve_local_file(f, db, dest_hint=dest) is_standalone,
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: if archive_key not in archive_entries:
# Hash fallback: the repo may hold the content under a in_repo = _name_in_index(
# different filename (exos21.rom vs exos21.bin). archive, by_name, by_path_suffix, data_names,
# by_name_lower,
# A copy contradicting the declared hash counts as held )
# only where the builder would ship it. Under existence archive_entries[archive_key] = {
# the frontend never opens the file, so the pack carries "profile": emu_name,
# it and reports the divergence; under a digest mode the "emulator": profile.get("emulator", emu_name),
# frontend would reject it, the builder leaves it out, and "systems": list(profile.get("systems", [])),
# calling it available here would describe a pack that "system": f.get("system"),
# does not contain it. "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) _lp, _st = resolve_local_file(f, db, dest_hint=dest)
in_repo = _st != "not_found" and _lp is not None in_repo = _lp is not None and _st != "hash_mismatch"
if in_repo and _st == "hash_mismatch" and shipped_on_mismatch: else:
in_repo = False 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")) checks = _parse_validation(f.get("validation"))
entry = { entry = {
"profile": emu_name, "profile": emu_name,
"emulator": profile.get("emulator", emu_name), "emulator": profile.get("emulator", emu_name),
"systems": list(profile.get("systems", [])), "systems": list(profile.get("systems", [])),
"system": f.get("system"), "system": f.get("system"),
"region": f.get("region"), "region": f.get("region"),
"variant_group": f.get("variant_group"), "variant_group": f.get("variant_group"),
"name": fname, "name": fname,
"path": dest, "path": dest,
"required": f.get("required", False), "required": f.get("required", False),
"hle_fallback": f.get("hle_fallback", False), "hle_fallback": f.get("hle_fallback", False),
"bundled": f.get("bundled", False), "bundled": f.get("bundled", False),
"category": f.get("category", "bios"), "category": f.get("category", "bios"),
"in_repo": in_repo, "in_repo": in_repo,
"note": f.get("note", ""), "note": f.get("note", ""),
"checks": sorted(checks) if checks else [], "checks": sorted(checks) if checks else [],
"source_ref": f.get("source_ref"), "source_ref": f.get("source_ref"),
"expected": _build_expected(f, checks), "expected": _build_expected(f, checks),
**_identity_of(f), **_identity_of(f),
"sha1": f.get("sha1"), "sha1": f.get("sha1"),
"md5": f.get("md5"), "md5": f.get("md5"),
} }
if f.get("contents"): if f.get("contents"):
entry["contents"] = f["contents"] entry["contents"] = f["contents"]
if duplicate: if duplicate:
first.setdefault("other_claims", []).append(entry) first.setdefault("other_claims", []).append(entry)
continue continue
reported[seen_key] = entry reported[seen_key] = entry
undeclared.append(entry) undeclared.append(entry)
# Append grouped archive entries # Append grouped archive entries
for entry in sorted(archive_entries.values(), key=lambda e: e["name"]): 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 # Count standalone-only files -but only report as excluded if the
# platform does NOT use this emulator in standalone mode # 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) is_standalone = runs_standalone(emu_name, profile, standalone_set)
if not is_standalone: if not is_standalone:
standalone_files = [ standalone_files = [
+56
View File
@@ -66,5 +66,61 @@ class TheTargetsNameTheStandaloneEmulator(unittest.TestCase):
self.assertEqual(mapping["BR2_PACKAGE_LIBRETRO_DOSBOX_STAGING"], ["dosbox_staging"]) 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 = ("<systemList><system><emulators>"
"<emulator name=\"libretro\"><cores><core>dolphin</core></cores></emulator>"
"<emulator name=\"dolphin\"/></emulators></system></systemList>")
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__": if __name__ == "__main__":
unittest.main() unittest.main()
+1 -1
View File
@@ -203,7 +203,7 @@ class RecalboxReadsItsSystemsInBatches(unittest.TestCase):
scraper = recalbox_scraper.Scraper.__new__(recalbox_scraper.Scraper) scraper = recalbox_scraper.Scraper.__new__(recalbox_scraper.Scraper)
scraper.tag = "10.1.1" scraper.tag = "10.1.1"
with mock.patch.object(recalbox_scraper, "fetch_bytes", fetch): 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(q) for q in queries], [50, 50, 20])
self.assertEqual(len(cores), 120) self.assertEqual(len(cores), 120)