From 7ac96f2b3958fdc66e85ca1a5003914e4bd425a9 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Wed, 21 Oct 2026 12:06:06 +0200 Subject: [PATCH 1/2] fix: check an entry's system: against its profile The semantic check read systems: only. mame2010 filed seven BIOS root sets under systems its list lacked: cpzn1, cpzn2 (zn.c:4543, 4563), konamigv (konamigv.c:877), skns (suprnova.c:1686), alg_bios (alg.c:782), maxaflex (maxaflex.c:541) and hng64 (hng64.c:2189) are GAME_IS_BIOS_ROOT in mame2010-libretro at source_commit. --- emulators/mame2010.yml | 7 +++++++ scripts/validate_schemas.py | 8 +++++--- tests/test_mame_devices.py | 10 +++++++++- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/emulators/mame2010.yml b/emulators/mame2010.yml index 26b15b78..6bdcea20 100644 --- a/emulators/mame2010.yml +++ b/emulators/mame2010.yml @@ -58,6 +58,13 @@ systems: - imax-macs - arcade - barcrest-mpu4 + - capcom-zn1 + - capcom-zn2 + - konami-gv + - super-kaneko-nova + - alg-laserdisc + - exidy-maxaflex + - hyper-neogeo64 notes: | Frozen snapshot of MAME 0.139 (mid-2010). Sourced from mame0139s.zip. diff --git a/scripts/validate_schemas.py b/scripts/validate_schemas.py index 0346cee9..c968d30b 100644 --- a/scripts/validate_schemas.py +++ b/scripts/validate_schemas.py @@ -74,7 +74,7 @@ _CHECK_FIELDS = { def _systems_outside_profile(path: Path, document: object) -> list[str]: - """An entry's systems: naming a system its profile does not list. + """An entry's system: or systems: naming a system its profile does not list. A --system pick selects the profile by its systems: and then the entry by its own, so a name the profile lacks is one no pick ever reaches. @@ -86,11 +86,13 @@ def _systems_outside_profile(path: Path, document: object) -> list[str]: for index, entry in enumerate(document.get("files") or []): if not isinstance(entry, dict): continue - unknown = [str(s) for s in entry.get("systems") or [] if str(s) not in listed] + named = [entry["system"]] if entry.get("system") else [] + named += list(entry.get("systems") or []) + unknown = [str(s) for s in named if str(s) not in listed] if unknown: out.append( f"{path.relative_to(ROOT)}: files[{index}] ({entry.get('name')}) " - f"systems {', '.join(unknown)} not in the profile's systems" + f"system {', '.join(unknown)} not in the profile's systems" ) return out diff --git a/tests/test_mame_devices.py b/tests/test_mame_devices.py index 60898895..2e679f98 100644 --- a/tests/test_mame_devices.py +++ b/tests/test_mame_devices.py @@ -622,7 +622,15 @@ class APickReadsTheEntrySystems(unittest.TestCase): outside = {**base, "files": [{"name": "x.zip", "systems": ["a", "c"]}]} found = validate_schemas._systems_outside_profile(root / "emulators" / "x.yml", outside) self.assertEqual(len(found), 1) - self.assertIn("systems c", found[0]) + self.assertIn("system c", found[0]) + single = {**base, "files": [{"name": "x.zip", "system": "c"}]} + found = validate_schemas._systems_outside_profile(root / "emulators" / "x.yml", single) + self.assertEqual(len(found), 1) + self.assertIn("system c", found[0]) + listed = {**base, "files": [{"name": "x.zip", "system": "a"}]} + self.assertEqual( + validate_schemas._systems_outside_profile(root / "emulators" / "x.yml", listed), [] + ) class Cli(unittest.TestCase): From 2d74524d59e3af31bab7dde016d8590548fc10ec Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Wed, 21 Oct 2026 12:24:37 +0200 Subject: [PATCH 2/2] feat: file mame software lists and floppy samples The -listxml names the software lists each machine and slot card declares and the floppy drives it can instantiate. --devices now writes one entry per hash/.xml the release ships under the systems of its placed machines (111 lists, 666 stay with hash/), and files each floppy sample under the systems whose drives select its form factor (floppy.cpp:1428-1473, 1759-1767). Machines a registered platform declares under one of the profile's systems seed the walk too: bbcb, cdimono1, cdimono2, dragon32, ep64 and ep128. Unplaced device sets go from 903 to 861. --- emulators/mame.yml | 1153 +++++++++++++++++++++++++- scripts/scraper/_hash_merge.py | 139 +++- scripts/scraper/mame_devices.py | 186 ++++- scripts/scraper/mame_hash_scraper.py | 82 +- scripts/upstream.py | 22 + tests/test_mame_devices.py | 341 +++++++- tests/test_upstream.py | 30 + wiki/testing-guide.md | 15 +- wiki/tools.md | 17 + 9 files changed, 1959 insertions(+), 26 deletions(-) diff --git a/emulators/mame.yml b/emulators/mame.yml index cb34d580..5cd99171 100644 --- a/emulators/mame.yml +++ b/emulators/mame.yml @@ -196,14 +196,30 @@ files: # Floppy drive sounds, the default sample list of floppy_sound_device. # Each name is tried as /.flac, floppy/.flac, then the # same as .wav, directly or inside floppy.zip. One sample missing and - # load_samples fails, so the drives stay silent. + # load_samples fails, so the drives stay silent. Every drive adds a floppy + # sound device, which loads the list its form factor selects: 3" and 3.5" + # drives the 35_ list, the others the 525_ list. Each sample is filed under + # the systems whose machines can instantiate such a drive. # ref: source src/devices/imagedev/floppy.cpp:1656-1685, 1709-1738, + # source src/devices/imagedev/floppy.cpp:1428-1473, 1759-1767, 2076-2082, # source src/devices/sound/samples.cpp:594-637 - name: "35_spin_start_empty.wav" path: "mame/samples/floppy/35_spin_start_empty.wav" mode: libretro category: game_data + systems: + - acorn-archimedes + - acorn-bbc-micro + - apple-macintosh-ii + - apple2gs + - coco + - coleco-adam + - dragon64 + - electron + - enterprise-64-128 + - pcw + - ti99 required: false size: 17708 sha1: "af770fd9261a8efee23d66cd39b27be7b2ab7853" @@ -219,6 +235,18 @@ files: path: "mame/samples/floppy/35_spin_start_loaded.wav" mode: libretro category: game_data + systems: + - acorn-archimedes + - acorn-bbc-micro + - apple-macintosh-ii + - apple2gs + - coco + - coleco-adam + - dragon64 + - electron + - enterprise-64-128 + - pcw + - ti99 required: false size: 35308 sha1: "c82b1714378bf2507ed86dea1e9fe743c380abab" @@ -229,6 +257,18 @@ files: path: "mame/samples/floppy/35_spin_empty.wav" mode: libretro category: game_data + systems: + - acorn-archimedes + - acorn-bbc-micro + - apple-macintosh-ii + - apple2gs + - coco + - coleco-adam + - dragon64 + - electron + - enterprise-64-128 + - pcw + - ti99 required: false size: 17708 sha1: "db04fc097bdd0411cff232f57525ded491b48c6c" @@ -239,6 +279,18 @@ files: path: "mame/samples/floppy/35_spin_loaded.wav" mode: libretro category: game_data + systems: + - acorn-archimedes + - acorn-bbc-micro + - apple-macintosh-ii + - apple2gs + - coco + - coleco-adam + - dragon64 + - electron + - enterprise-64-128 + - pcw + - ti99 required: false size: 17708 sha1: "96ce04a5b02af0da6d311f06fc4906a3cf71c78f" @@ -249,6 +301,18 @@ files: path: "mame/samples/floppy/35_spin_end.wav" mode: libretro category: game_data + systems: + - acorn-archimedes + - acorn-bbc-micro + - apple-macintosh-ii + - apple2gs + - coco + - coleco-adam + - dragon64 + - electron + - enterprise-64-128 + - pcw + - ti99 required: false size: 17684 sha1: "a8901b692de15eb7a9052f61283f0e7a2b63d67d" @@ -259,6 +323,18 @@ files: path: "mame/samples/floppy/35_step_1_1.wav" mode: libretro category: game_data + systems: + - acorn-archimedes + - acorn-bbc-micro + - apple-macintosh-ii + - apple2gs + - coco + - coleco-adam + - dragon64 + - electron + - enterprise-64-128 + - pcw + - ti99 required: false size: 6878 sha1: "40d756d0cb5f7998ca26ac1de142a96a83fa2b97" @@ -269,6 +345,18 @@ files: path: "mame/samples/floppy/35_seek_2ms.wav" mode: libretro category: game_data + systems: + - acorn-archimedes + - acorn-bbc-micro + - apple-macintosh-ii + - apple2gs + - coco + - coleco-adam + - dragon64 + - electron + - enterprise-64-128 + - pcw + - ti99 required: false size: 7450 sha1: "55caafbd19486b8d517e4b33a6fa65250df55e25" @@ -279,6 +367,18 @@ files: path: "mame/samples/floppy/35_seek_6ms.wav" mode: libretro category: game_data + systems: + - acorn-archimedes + - acorn-bbc-micro + - apple-macintosh-ii + - apple2gs + - coco + - coleco-adam + - dragon64 + - electron + - enterprise-64-128 + - pcw + - ti99 required: false size: 22230 sha1: "87e5dec269e67497681c15849777ebfc301131c1" @@ -289,6 +389,18 @@ files: path: "mame/samples/floppy/35_seek_12ms.wav" mode: libretro category: game_data + systems: + - acorn-archimedes + - acorn-bbc-micro + - apple-macintosh-ii + - apple2gs + - coco + - coleco-adam + - dragon64 + - electron + - enterprise-64-128 + - pcw + - ti99 required: false size: 26540 sha1: "c6b34fc61496943134ac45cc836a24e743caf794" @@ -299,6 +411,18 @@ files: path: "mame/samples/floppy/35_seek_20ms.wav" mode: libretro category: game_data + systems: + - acorn-archimedes + - acorn-bbc-micro + - apple-macintosh-ii + - apple2gs + - coco + - coleco-adam + - dragon64 + - electron + - enterprise-64-128 + - pcw + - ti99 required: false size: 44144 sha1: "b2fe4c3a4185146a7d11483d92d4412dd79dbc78" @@ -309,6 +433,23 @@ files: path: "mame/samples/floppy/525_spin_start_empty.wav" mode: libretro category: game_data + systems: + - acorn-archimedes + - acorn-bbc-micro + - apf-m1000 + - apple-macintosh-ii + - apple2gs + - camplynx + - coco + - coleco-adam + - dragon64 + - electron + - elektronika-bk + - fm7 + - laser310 + - pc80 + - ti99 + - trs80 required: false size: 11180 sha1: "b87d89c4b46c36dd78dec3dc5170ca61501e7cd9" @@ -319,6 +460,23 @@ files: path: "mame/samples/floppy/525_spin_start_loaded.wav" mode: libretro category: game_data + systems: + - acorn-archimedes + - acorn-bbc-micro + - apf-m1000 + - apple-macintosh-ii + - apple2gs + - camplynx + - coco + - coleco-adam + - dragon64 + - electron + - elektronika-bk + - fm7 + - laser310 + - pc80 + - ti99 + - trs80 required: false size: 17708 sha1: "510476098848e2b5cdfe9f1608f8dfa3207fe9b4" @@ -329,6 +487,23 @@ files: path: "mame/samples/floppy/525_spin_empty.wav" mode: libretro category: game_data + systems: + - acorn-archimedes + - acorn-bbc-micro + - apf-m1000 + - apple-macintosh-ii + - apple2gs + - camplynx + - coco + - coleco-adam + - dragon64 + - electron + - elektronika-bk + - fm7 + - laser310 + - pc80 + - ti99 + - trs80 required: false size: 8864 sha1: "4b13701d9a7f8ab0f0827f17ba80b5c32b4e0fa7" @@ -339,6 +514,23 @@ files: path: "mame/samples/floppy/525_spin_loaded.wav" mode: libretro category: game_data + systems: + - acorn-archimedes + - acorn-bbc-micro + - apf-m1000 + - apple-macintosh-ii + - apple2gs + - camplynx + - coco + - coleco-adam + - dragon64 + - electron + - elektronika-bk + - fm7 + - laser310 + - pc80 + - ti99 + - trs80 required: false size: 17708 sha1: "875f90538a3ff5db863ad4c384e1808841ddea9b" @@ -349,6 +541,23 @@ files: path: "mame/samples/floppy/525_spin_end.wav" mode: libretro category: game_data + systems: + - acorn-archimedes + - acorn-bbc-micro + - apf-m1000 + - apple-macintosh-ii + - apple2gs + - camplynx + - coco + - coleco-adam + - dragon64 + - electron + - elektronika-bk + - fm7 + - laser310 + - pc80 + - ti99 + - trs80 required: false size: 8864 sha1: "f8d9b94c4d92d35d9ffb1117b693fc74a93f613f" @@ -359,6 +568,23 @@ files: path: "mame/samples/floppy/525_step_1_1.wav" mode: libretro category: game_data + systems: + - acorn-archimedes + - acorn-bbc-micro + - apf-m1000 + - apple-macintosh-ii + - apple2gs + - camplynx + - coco + - coleco-adam + - dragon64 + - electron + - elektronika-bk + - fm7 + - laser310 + - pc80 + - ti99 + - trs80 required: false size: 6628 sha1: "669ee98fb9fac4371c72ed0b60be1bc8470bc7f8" @@ -369,6 +595,23 @@ files: path: "mame/samples/floppy/525_seek_6ms.wav" mode: libretro category: game_data + systems: + - acorn-archimedes + - acorn-bbc-micro + - apf-m1000 + - apple-macintosh-ii + - apple2gs + - camplynx + - coco + - coleco-adam + - dragon64 + - electron + - elektronika-bk + - fm7 + - laser310 + - pc80 + - ti99 + - trs80 required: false size: 14860 sha1: "6f698055fa6e9da6c5ae3fd6fb05de76b92a45a9" @@ -379,6 +622,23 @@ files: path: "mame/samples/floppy/525_seek_12ms.wav" mode: libretro category: game_data + systems: + - acorn-archimedes + - acorn-bbc-micro + - apf-m1000 + - apple-macintosh-ii + - apple2gs + - camplynx + - coco + - coleco-adam + - dragon64 + - electron + - elektronika-bk + - fm7 + - laser310 + - pc80 + - ti99 + - trs80 required: false size: 22764 sha1: "fbbbb5e8724c076d194e66a4443d299d54a8efd6" @@ -389,6 +649,23 @@ files: path: "mame/samples/floppy/525_seek_20ms.wav" mode: libretro category: game_data + systems: + - acorn-archimedes + - acorn-bbc-micro + - apf-m1000 + - apple-macintosh-ii + - apple2gs + - camplynx + - coco + - coleco-adam + - dragon64 + - electron + - elektronika-bk + - fm7 + - laser310 + - pc80 + - ti99 + - trs80 required: false size: 30060 sha1: "b6cd14944454c743861196edb4fcab1cc9b586d4" @@ -5917,6 +6194,807 @@ files: crc32: "604415c8" sha1: "6c67c3be1aca03d009b6244e3bd911caaa2ca666" + # --- Software lists (generated from MAME -listxml) --- + # software_list_device::parse opens .xml for each software list a + # machine declares. Each list below is filed under the systems whose + # machines declare it, a list no such machine declares stays with its + # hash/ tree. + # ref: upstream src/emu/softlist_dev.cpp:281-313, + # upstream src/frontend/mame/infoxml.cpp:2184-2198 + + - name: "3do.xml" + path: "mame/hash/3do.xml" + mode: libretro + category: game_data + required: false + system: panasonic-3do + + - name: "a800.xml" + path: "mame/hash/a800.xml" + mode: libretro + category: game_data + required: false + system: xegs + + - name: "a800_cass.xml" + path: "mame/hash/a800_cass.xml" + mode: libretro + category: game_data + required: false + system: xegs + + - name: "a800_flop.xml" + path: "mame/hash/a800_flop.xml" + mode: libretro + category: game_data + required: false + system: xegs + + - name: "adam_cart.xml" + path: "mame/hash/adam_cart.xml" + mode: libretro + category: game_data + required: false + system: coleco-adam + + - name: "adam_cass.xml" + path: "mame/hash/adam_cass.xml" + mode: libretro + category: game_data + required: false + system: coleco-adam + + - name: "adam_flop.xml" + path: "mame/hash/adam_flop.xml" + mode: libretro + category: game_data + required: false + system: coleco-adam + + - name: "advision.xml" + path: "mame/hash/advision.xml" + mode: libretro + category: game_data + required: false + system: entex-advision + + - name: "apfimag_cass.xml" + path: "mame/hash/apfimag_cass.xml" + mode: libretro + category: game_data + required: false + system: apf-m1000 + + - name: "apfm1000.xml" + path: "mame/hash/apfm1000.xml" + mode: libretro + category: game_data + required: false + system: apf-m1000 + + - name: "apple2_flop_clcracked.xml" + path: "mame/hash/apple2_flop_clcracked.xml" + mode: libretro + category: game_data + required: false + system: apple2gs + + - name: "apple2_flop_misc.xml" + path: "mame/hash/apple2_flop_misc.xml" + mode: libretro + category: game_data + required: false + system: apple2gs + + - name: "apple2_flop_orig.xml" + path: "mame/hash/apple2_flop_orig.xml" + mode: libretro + category: game_data + required: false + system: apple2gs + + - name: "apple2gs_flop_clcracked.xml" + path: "mame/hash/apple2gs_flop_clcracked.xml" + mode: libretro + category: game_data + required: false + system: apple2gs + + - name: "apple2gs_flop_misc.xml" + path: "mame/hash/apple2gs_flop_misc.xml" + mode: libretro + category: game_data + required: false + system: apple2gs + + - name: "apple2gs_flop_orig.xml" + path: "mame/hash/apple2gs_flop_orig.xml" + mode: libretro + category: game_data + required: false + system: apple2gs + + - name: "archimedes.xml" + path: "mame/hash/archimedes.xml" + mode: libretro + category: game_data + required: false + system: acorn-archimedes + + - name: "archimedes_hdd.xml" + path: "mame/hash/archimedes_hdd.xml" + mode: libretro + category: game_data + required: false + system: acorn-archimedes + + - name: "archimedes_rom.xml" + path: "mame/hash/archimedes_rom.xml" + mode: libretro + category: game_data + required: false + system: acorn-archimedes + + - name: "astrocde.xml" + path: "mame/hash/astrocde.xml" + mode: libretro + category: game_data + required: false + system: bally-astrocade + + - name: "bbc_cass.xml" + path: "mame/hash/bbc_cass.xml" + mode: libretro + category: game_data + required: false + systems: + - acorn-bbc-micro + - electron + + - name: "bbc_flop_32016.xml" + path: "mame/hash/bbc_flop_32016.xml" + mode: libretro + category: game_data + required: false + system: acorn-bbc-micro + + - name: "bbc_flop_6502.xml" + path: "mame/hash/bbc_flop_6502.xml" + mode: libretro + category: game_data + required: false + system: acorn-bbc-micro + + - name: "bbc_flop_68000.xml" + path: "mame/hash/bbc_flop_68000.xml" + mode: libretro + category: game_data + required: false + system: acorn-bbc-micro + + - name: "bbc_flop_80186.xml" + path: "mame/hash/bbc_flop_80186.xml" + mode: libretro + category: game_data + required: false + system: acorn-bbc-micro + + - name: "bbc_flop_arm.xml" + path: "mame/hash/bbc_flop_arm.xml" + mode: libretro + category: game_data + required: false + system: acorn-bbc-micro + + - name: "bbc_flop_hybrid.xml" + path: "mame/hash/bbc_flop_hybrid.xml" + mode: libretro + category: game_data + required: false + systems: + - acorn-archimedes + - acorn-bbc-micro + + - name: "bbc_flop_torch.xml" + path: "mame/hash/bbc_flop_torch.xml" + mode: libretro + category: game_data + required: false + system: acorn-bbc-micro + + - name: "bbc_flop_z80.xml" + path: "mame/hash/bbc_flop_z80.xml" + mode: libretro + category: game_data + required: false + systems: + - acorn-archimedes + - acorn-bbc-micro + + - name: "bbc_hdd.xml" + path: "mame/hash/bbc_hdd.xml" + mode: libretro + category: game_data + required: false + system: acorn-bbc-micro + + - name: "bbc_rom.xml" + path: "mame/hash/bbc_rom.xml" + mode: libretro + category: game_data + required: false + systems: + - acorn-archimedes + - acorn-bbc-micro + + - name: "bbc_vsm.xml" + path: "mame/hash/bbc_vsm.xml" + mode: libretro + category: game_data + required: false + system: acorn-bbc-micro + + - name: "bbcb_flop.xml" + path: "mame/hash/bbcb_flop.xml" + mode: libretro + category: game_data + required: false + system: acorn-bbc-micro + + - name: "bbcb_flop_orig.xml" + path: "mame/hash/bbcb_flop_orig.xml" + mode: libretro + category: game_data + required: false + system: acorn-bbc-micro + + - name: "bbcb_flop_us.xml" + path: "mame/hash/bbcb_flop_us.xml" + mode: libretro + category: game_data + required: false + system: acorn-bbc-micro + + - name: "bbcm_cart.xml" + path: "mame/hash/bbcm_cart.xml" + mode: libretro + category: game_data + required: false + system: acorn-bbc-micro + + - name: "bbcm_flop.xml" + path: "mame/hash/bbcm_flop.xml" + mode: libretro + category: game_data + required: false + system: acorn-bbc-micro + + - name: "bbcmc_flop.xml" + path: "mame/hash/bbcmc_flop.xml" + mode: libretro + category: game_data + required: false + system: acorn-bbc-micro + + - name: "bk0010.xml" + path: "mame/hash/bk0010.xml" + mode: libretro + category: game_data + required: false + system: elektronika-bk + + - name: "camplynx_cass.xml" + path: "mame/hash/camplynx_cass.xml" + mode: libretro + category: game_data + required: false + system: camplynx + + - name: "camplynx_flop.xml" + path: "mame/hash/camplynx_flop.xml" + mode: libretro + category: game_data + required: false + system: camplynx + + - name: "casloopy.xml" + path: "mame/hash/casloopy.xml" + mode: libretro + category: game_data + required: false + system: casio-loopy + + - name: "cdi.xml" + path: "mame/hash/cdi.xml" + mode: libretro + category: game_data + required: false + system: philips-cdi + + - name: "coco_cart.xml" + path: "mame/hash/coco_cart.xml" + mode: libretro + category: game_data + required: false + systems: + - coco + - dragon64 + + - name: "coco_flop.xml" + path: "mame/hash/coco_flop.xml" + mode: libretro + category: game_data + required: false + system: coco + + - name: "coleco.xml" + path: "mame/hash/coleco.xml" + mode: libretro + category: game_data + required: false + system: coleco-adam + + - name: "coleco_homebrew.xml" + path: "mame/hash/coleco_homebrew.xml" + mode: libretro + category: game_data + required: false + system: coleco-adam + + - name: "crvision.xml" + path: "mame/hash/crvision.xml" + mode: libretro + category: game_data + required: false + system: crvision + + - name: "dgnalpha_flop.xml" + path: "mame/hash/dgnalpha_flop.xml" + mode: libretro + category: game_data + required: false + system: dragon64 + + - name: "dragon_cart.xml" + path: "mame/hash/dragon_cart.xml" + mode: libretro + category: game_data + required: false + systems: + - coco + - dragon64 + + - name: "dragon_cass.xml" + path: "mame/hash/dragon_cass.xml" + mode: libretro + category: game_data + required: false + system: dragon64 + + - name: "dragon_flex.xml" + path: "mame/hash/dragon_flex.xml" + mode: libretro + category: game_data + required: false + system: dragon64 + + - name: "dragon_flop.xml" + path: "mame/hash/dragon_flop.xml" + mode: libretro + category: game_data + required: false + system: dragon64 + + - name: "dragon_os9.xml" + path: "mame/hash/dragon_os9.xml" + mode: libretro + category: game_data + required: false + system: dragon64 + + - name: "e01_flop.xml" + path: "mame/hash/e01_flop.xml" + mode: libretro + category: game_data + required: false + systems: + - acorn-archimedes + - acorn-bbc-micro + + - name: "electron_cart.xml" + path: "mame/hash/electron_cart.xml" + mode: libretro + category: game_data + required: false + system: electron + + - name: "electron_cass.xml" + path: "mame/hash/electron_cass.xml" + mode: libretro + category: game_data + required: false + system: electron + + - name: "electron_flop.xml" + path: "mame/hash/electron_flop.xml" + mode: libretro + category: game_data + required: false + system: electron + + - name: "electron_rom.xml" + path: "mame/hash/electron_rom.xml" + mode: libretro + category: game_data + required: false + system: electron + + - name: "ep64_cart.xml" + path: "mame/hash/ep64_cart.xml" + mode: libretro + category: game_data + required: false + system: enterprise-64-128 + + - name: "ep64_cass.xml" + path: "mame/hash/ep64_cass.xml" + mode: libretro + category: game_data + required: false + system: enterprise-64-128 + + - name: "ep64_flop.xml" + path: "mame/hash/ep64_flop.xml" + mode: libretro + category: game_data + required: false + system: enterprise-64-128 + + - name: "fm77av.xml" + path: "mame/hash/fm77av.xml" + mode: libretro + category: game_data + required: false + system: fm7 + + - name: "fm7_cass.xml" + path: "mame/hash/fm7_cass.xml" + mode: libretro + category: game_data + required: false + system: fm7 + + - name: "fm7_disk.xml" + path: "mame/hash/fm7_disk.xml" + mode: libretro + category: game_data + required: false + system: fm7 + + - name: "fm8_cass.xml" + path: "mame/hash/fm8_cass.xml" + mode: libretro + category: game_data + required: false + system: fm7 + + - name: "gamate.xml" + path: "mame/hash/gamate.xml" + mode: libretro + category: game_data + required: false + system: gamate + + - name: "gamecom.xml" + path: "mame/hash/gamecom.xml" + mode: libretro + category: game_data + required: false + system: tiger-game-com + + - name: "gamepock.xml" + path: "mame/hash/gamepock.xml" + mode: libretro + category: game_data + required: false + system: gamepock + + - name: "gmaster.xml" + path: "mame/hash/gmaster.xml" + mode: libretro + category: game_data + required: false + system: hartung-game-master + + - name: "gp32.xml" + path: "mame/hash/gp32.xml" + mode: libretro + category: game_data + required: false + system: gamepark-gp32 + + - name: "ibm5150.xml" + path: "mame/hash/ibm5150.xml" + mode: libretro + category: game_data + required: false + system: acorn-bbc-micro + + - name: "laser2001_cart.xml" + path: "mame/hash/laser2001_cart.xml" + mode: libretro + category: game_data + required: false + system: crvision + + - name: "laser2001_flop.xml" + path: "mame/hash/laser2001_flop.xml" + mode: libretro + category: game_data + required: false + system: crvision + + - name: "mac_cdrom.xml" + path: "mame/hash/mac_cdrom.xml" + mode: libretro + category: game_data + required: false + system: apple-macintosh-ii + + - name: "mac_flop.xml" + path: "mame/hash/mac_flop.xml" + mode: libretro + category: game_data + required: false + system: apple-macintosh-ii + + - name: "mac_flop_clcracked.xml" + path: "mame/hash/mac_flop_clcracked.xml" + mode: libretro + category: game_data + required: false + system: apple-macintosh-ii + + - name: "mac_flop_orig.xml" + path: "mame/hash/mac_flop_orig.xml" + mode: libretro + category: game_data + required: false + system: apple-macintosh-ii + + - name: "mac_hdd.xml" + path: "mame/hash/mac_hdd.xml" + mode: libretro + category: game_data + required: false + system: apple-macintosh-ii + + - name: "mac_hdflop.xml" + path: "mame/hash/mac_hdflop.xml" + mode: libretro + category: game_data + required: false + system: apple-macintosh-ii + + - name: "mc10.xml" + path: "mame/hash/mc10.xml" + mode: libretro + category: game_data + required: false + system: mc10 + + - name: "megatech.xml" + path: "mame/hash/megatech.xml" + mode: libretro + category: game_data + required: false + system: sega-megatech + + - name: "neogeo.xml" + path: "mame/hash/neogeo.xml" + mode: libretro + category: game_data + required: false + system: snk-neogeo-mvs + + - name: "pc8001_flop.xml" + path: "mame/hash/pc8001_flop.xml" + mode: libretro + category: game_data + required: false + system: pc80 + + - name: "pc8001mk2_flop.xml" + path: "mame/hash/pc8001mk2_flop.xml" + mode: libretro + category: game_data + required: false + system: pc80 + + - name: "pc8001mk2sr_flop.xml" + path: "mame/hash/pc8001mk2sr_flop.xml" + mode: libretro + category: game_data + required: false + system: pc80 + + - name: "pcw.xml" + path: "mame/hash/pcw.xml" + mode: libretro + category: game_data + required: false + system: pcw + + - name: "pegasus_cart.xml" + path: "mame/hash/pegasus_cart.xml" + mode: libretro + category: game_data + required: false + system: pegasus + + - name: "photo_cd.xml" + path: "mame/hash/photo_cd.xml" + mode: libretro + category: game_data + required: false + systems: + - panasonic-3do + - philips-cdi + + - name: "pro128s_flop.xml" + path: "mame/hash/pro128s_flop.xml" + mode: libretro + category: game_data + required: false + system: acorn-bbc-micro + + - name: "pv2000.xml" + path: "mame/hash/pv2000.xml" + mode: libretro + category: game_data + required: false + system: casio-pv2000 + + - name: "rx78_cart.xml" + path: "mame/hash/rx78_cart.xml" + mode: libretro + category: game_data + required: false + system: bandai-rx78 + + - name: "rx78_cass.xml" + path: "mame/hash/rx78_cass.xml" + mode: libretro + category: game_data + required: false + system: bandai-rx78 + + - name: "scv.xml" + path: "mame/hash/scv.xml" + mode: libretro + category: game_data + required: false + system: scv + + - name: "sega_beena_cart.xml" + path: "mame/hash/sega_beena_cart.xml" + mode: libretro + category: game_data + required: false + system: sega-beena + + - name: "segaai.xml" + path: "mame/hash/segaai.xml" + mode: libretro + category: game_data + required: false + system: sega-ai + + - name: "socrates.xml" + path: "mame/hash/socrates.xml" + mode: libretro + category: game_data + required: false + system: vtech-socrates + + - name: "stv.xml" + path: "mame/hash/stv.xml" + mode: libretro + category: game_data + required: false + system: sega-stv + + - name: "ti99_cart.xml" + path: "mame/hash/ti99_cart.xml" + mode: libretro + category: game_data + required: false + system: ti99 + + - name: "trs80_cass.xml" + path: "mame/hash/trs80_cass.xml" + mode: libretro + category: game_data + required: false + system: trs80 + + - name: "trs80_flop.xml" + path: "mame/hash/trs80_flop.xml" + mode: libretro + category: game_data + required: false + system: trs80 + + - name: "trs80_quik.xml" + path: "mame/hash/trs80_quik.xml" + mode: libretro + category: game_data + required: false + system: trs80 + + - name: "tutor.xml" + path: "mame/hash/tutor.xml" + mode: libretro + category: game_data + required: false + system: tutor + + - name: "vectrex.xml" + path: "mame/hash/vectrex.xml" + mode: libretro + category: game_data + required: false + system: vectrex + + - name: "vg5k.xml" + path: "mame/hash/vg5k.xml" + mode: libretro + category: game_data + required: false + system: philips-vg5k + + - name: "vis.xml" + path: "mame/hash/vis.xml" + mode: libretro + category: game_data + required: false + system: tandy-vis + + - name: "vsmile_cart.xml" + path: "mame/hash/vsmile_cart.xml" + mode: libretro + category: game_data + required: false + system: vtech-vsmile + + - name: "vsmilem_cart.xml" + path: "mame/hash/vsmilem_cart.xml" + mode: libretro + category: game_data + required: false + system: vtech-vsmile + + - name: "vz_cass.xml" + path: "mame/hash/vz_cass.xml" + mode: libretro + category: game_data + required: false + system: laser310 + + - name: "vz_snap.xml" + path: "mame/hash/vz_snap.xml" + mode: libretro + category: game_data + required: false + system: laser310 + + - name: "xegs.xml" + path: "mame/hash/xegs.xml" + mode: libretro + category: game_data + required: false + system: xegs + # --- Device ROM sets (generated from MAME -listxml) --- - name: 22vp931.zip required: false @@ -8911,6 +9989,7 @@ files: - dragon64 - electron - elektronika-bk + - enterprise-64-128 - fm7 - konami-twinkle - laser310 @@ -10066,6 +11145,7 @@ files: - name: bbc_acorn1770.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/bbc/fdc/acorn.cpp:79-93" note: >- Device ROM set, loaded when a machine instantiates `Acorn 1770 FDC`. @@ -10099,6 +11179,7 @@ files: - name: bbc_acorn8271.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/bbc/fdc/acorn.cpp:52-77" note: >- Device ROM set, loaded when a machine instantiates `Acorn 8721 FDC`. @@ -10152,6 +11233,7 @@ files: - name: bbc_ams3.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/bbc/fdc/ams.cpp:37-45" note: >- Device ROM set, loaded when a machine instantiates `AMS 3" Microdrive @@ -10169,6 +11251,7 @@ files: - name: bbc_ariesb20.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/bbc/internal/aries.cpp:34-40" note: >- Device ROM set, loaded when a machine instantiates `Aries-B20 RAM @@ -10188,6 +11271,7 @@ files: - name: bbc_ariesb32.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/bbc/internal/aries.cpp:42-46" note: >- Device ROM set, loaded when a machine instantiates `Aries-B32 RAM @@ -10277,6 +11361,7 @@ files: - name: bbc_cisco.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/bbc/1mhzbus/cisco.cpp:49-56" note: >- Device ROM set, loaded when a machine instantiates `Cisco Terminal Data @@ -10306,6 +11391,7 @@ files: - name: bbc_cms6502.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/bbc/tube/cms6502.cpp:66-75" note: >- Device ROM set, loaded when a machine instantiates `CMS 6502 2nd @@ -10327,6 +11413,7 @@ files: - name: bbc_cumana1.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/bbc/fdc/cumana.cpp:51-56" note: >- Device ROM set, loaded when a machine instantiates `Cumana QFS 8877A @@ -10341,6 +11428,7 @@ files: - name: bbc_cumana2.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/bbc/fdc/cumana.cpp:58-63" note: >- Device ROM set, loaded when a machine instantiates `Cumana QFS Issue 2 @@ -10355,6 +11443,7 @@ files: - name: bbc_cumana68k.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/bbc/internal/cumana68k.cpp:59-62" note: >- Device ROM set, loaded when a machine instantiates `Cumana 68008 Upgrade @@ -10368,6 +11457,7 @@ files: - name: bbc_cv1797.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/bbc/fdc/cv1797.cpp:54-60" note: >- Device ROM set, loaded when a machine instantiates `Computer Village @@ -10446,6 +11536,7 @@ files: - name: bbc_integrab.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/bbc/internal/integrab.cpp:56-64" note: >- Device ROM set, loaded when a machine instantiates `Computech @@ -10470,6 +11561,7 @@ files: - name: bbc_kenda.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/bbc/fdc/kenda.cpp:64-68" note: >- Device ROM set, loaded when a machine instantiates `Kenda Professional @@ -10497,6 +11589,7 @@ files: - name: bbc_memexb20.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/bbc/internal/memex.cpp:28-31" note: >- Device ROM set, loaded when a machine instantiates `Memex-B20 RAM @@ -10557,6 +11650,7 @@ files: - name: bbc_opus1770.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/bbc/fdc/opus.cpp:90-97" note: >- Device ROM set, loaded when a machine instantiates `Opus D-DOS(B) 1770 @@ -10576,6 +11670,7 @@ files: - name: bbc_opus2791.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/bbc/fdc/opus.cpp:68-79" note: >- Device ROM set, loaded when a machine instantiates `Opus 2791 FDC`. @@ -10604,6 +11699,7 @@ files: - name: bbc_opus2793.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/bbc/fdc/opus.cpp:81-88" note: >- Device ROM set, loaded when a machine instantiates `Opus 2793 FDC`. @@ -10649,6 +11745,7 @@ files: - name: bbc_opus8272.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/bbc/fdc/opus.cpp:59-66" note: >- Device ROM set, loaded when a machine instantiates `Opus 8272 FDC`. @@ -10730,6 +11827,7 @@ files: - name: bbc_raven20.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/bbc/internal/raven20.cpp:29-35" note: >- Device ROM set, loaded when a machine instantiates `Raven-20 RAM @@ -10749,6 +11847,7 @@ files: - name: bbc_stl1770_1.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/bbc/fdc/solidisk.cpp:77-81" note: >- Device ROM set, loaded when a machine instantiates `Solidisk 1770 DDFS @@ -10763,6 +11862,7 @@ files: - name: bbc_stl1770_2.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/bbc/fdc/solidisk.cpp:83-87" note: >- Device ROM set, loaded when a machine instantiates `Solidisk 1770 DDFS @@ -10777,6 +11877,7 @@ files: - name: bbc_stl2m128.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/bbc/internal/stl2m128.cpp:45-49" note: >- Device ROM set, loaded when a machine instantiates `Solidisk Twomeg 128K @@ -10790,6 +11891,7 @@ files: - name: bbc_stl4m32.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/bbc/internal/stl4m32.cpp:49-53" note: >- Device ROM set, loaded when a machine instantiates `Solidisk Fourmeg 32K @@ -10804,6 +11906,7 @@ files: - name: bbc_stldfdc_1.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/bbc/fdc/solidisk.cpp:89-93" note: >- Device ROM set, loaded when a machine instantiates `Solidisk 8271/1770 @@ -10818,6 +11921,7 @@ files: - name: bbc_stlswr128.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/bbc/internal/stlswr.cpp:30-34" note: >- Device ROM set, loaded when a machine instantiates `Solidisk SWR128 - @@ -10832,6 +11936,7 @@ files: - name: bbc_stlswr16.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/bbc/internal/stlswr.cpp:30-34" note: >- Device ROM set, loaded when a machine instantiates `Solidisk SWR16 - 16K @@ -10846,6 +11951,7 @@ files: - name: bbc_stlswr32.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/bbc/internal/stlswr.cpp:30-34" note: >- Device ROM set, loaded when a machine instantiates `Solidisk SWR32 - 32K @@ -10860,6 +11966,7 @@ files: - name: bbc_stlswr64.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/bbc/internal/stlswr.cpp:30-34" note: >- Device ROM set, loaded when a machine instantiates `Solidisk SWR64 - 64K @@ -10874,6 +11981,7 @@ files: - name: bbc_sweetalker.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/bbc/vsp/sweetalker.cpp:48-51" note: >- Device ROM set, loaded when a machine instantiates `Cheetah Sweet Talker @@ -10887,6 +11995,7 @@ files: - name: bbc_tube_16032.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/bbc/tube/tube_32016.cpp:144-156" note: >- Device ROM set, loaded when a machine instantiates `Acorn 16032 2nd @@ -11017,6 +12126,7 @@ files: - name: bbc_tube_6502p.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/bbc/tube/tube_6502.cpp:211-221" note: >- Device ROM set, loaded when a machine instantiates `Acorn 6502 2nd @@ -11191,6 +12301,7 @@ files: - name: bbc_tube_casper.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/bbc/tube/tube_casper.cpp:70-78" note: >- Device ROM set, loaded when a machine instantiates `Casper 68000 2nd @@ -11216,6 +12327,7 @@ files: - name: bbc_tube_cms6809.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/bbc/tube/tube_cms6809.cpp:81-87" note: >- Device ROM set, loaded when a machine instantiates `CMS 6809 2nd @@ -11331,6 +12443,7 @@ files: - name: bbc_tube_x25.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/bbc/tube/tube_x25.cpp:115-122" note: >- Device ROM set, loaded when a machine instantiates `Econet X25 Gateway @@ -11382,6 +12495,7 @@ files: - name: bbc_tube_zep100.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/bbc/tube/tube_zep100.cpp:152-167" note: >- Device ROM set, loaded when a machine instantiates `Torch Z80 @@ -11416,6 +12530,7 @@ files: - name: bbc_tube_zep100l.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/bbc/tube/tube_zep100.cpp:169-176" note: >- Device ROM set, loaded when a machine instantiates `Torch Z80 @@ -11458,6 +12573,7 @@ files: - name: bbc_tube_zep100w.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/bbc/tube/tube_zep100.cpp:178-187" note: >- Device ROM set, loaded when a machine instantiates `Torch Z80 @@ -11481,6 +12597,7 @@ files: - name: bbc_udm.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/bbc/fdc/udm.cpp:49-59" note: >- Device ROM set, loaded when a machine instantiates `United Disk Memories @@ -11527,6 +12644,7 @@ files: - name: bbc_we32kram.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/bbc/internal/we32kram.cpp:27-39" note: >- Device ROM set, loaded when a machine instantiates `Watford Electronics @@ -11561,6 +12679,7 @@ files: - name: bbc_weddb2.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/bbc/fdc/watford.cpp:53-62" note: >- Device ROM set, loaded when a machine instantiates `Watford Electronics @@ -11585,6 +12704,7 @@ files: - name: bbc_weddb3.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/bbc/fdc/watford.cpp:64-69" note: >- Device ROM set, loaded when a machine instantiates `Watford Electronics @@ -13508,6 +14628,7 @@ files: - dragon64 - electron - elektronika-bk + - enterprise-64-128 - fm7 - konami-twinkle - laser310 @@ -13798,6 +14919,7 @@ files: - name: cms_4080term.zip required: false category: bios_zip + system: acorn-bbc-micro source_ref: "upstream src/devices/bus/acorn/cms/4080term.cpp:96-99" note: >- Device ROM set, loaded when a machine instantiates `CMS 40/80 Video @@ -14125,6 +15247,7 @@ files: - dragon64 - electron - elektronika-bk + - enterprise-64-128 - fm7 - konami-twinkle - laser310 @@ -16215,6 +17338,7 @@ files: - dragon64 - electron - elektronika-bk + - enterprise-64-128 - fm7 - konami-twinkle - laser310 @@ -16247,6 +17371,7 @@ files: - dragon64 - electron - elektronika-bk + - enterprise-64-128 - fm7 - konami-twinkle - laser310 @@ -16329,6 +17454,7 @@ files: - dragon64 - electron - elektronika-bk + - enterprise-64-128 - fm7 - konami-twinkle - laser310 @@ -16525,6 +17651,7 @@ files: - dragon64 - electron - elektronika-bk + - enterprise-64-128 - fm7 - konami-twinkle - laser310 @@ -17289,6 +18416,7 @@ files: - dragon64 - electron - elektronika-bk + - enterprise-64-128 - konami-twinkle - laser310 - mc10 @@ -17422,6 +18550,7 @@ files: - dragon64 - electron - elektronika-bk + - enterprise-64-128 - konami-twinkle - laser310 - mc10 @@ -17468,6 +18597,7 @@ files: - dragon64 - electron - elektronika-bk + - enterprise-64-128 - konami-twinkle - laser310 - mc10 @@ -17504,6 +18634,7 @@ files: - dragon64 - electron - elektronika-bk + - enterprise-64-128 - konami-twinkle - laser310 - mc10 @@ -17540,6 +18671,7 @@ files: - dragon64 - electron - elektronika-bk + - enterprise-64-128 - konami-twinkle - laser310 - mc10 @@ -17576,6 +18708,7 @@ files: - dragon64 - electron - elektronika-bk + - enterprise-64-128 - konami-twinkle - laser310 - mc10 @@ -17612,6 +18745,7 @@ files: - dragon64 - electron - elektronika-bk + - enterprise-64-128 - konami-twinkle - laser310 - mc10 @@ -17913,6 +19047,7 @@ files: - dragon64 - electron - elektronika-bk + - enterprise-64-128 - konami-twinkle - laser310 - mc10 @@ -17950,6 +19085,7 @@ files: - dragon64 - electron - elektronika-bk + - enterprise-64-128 - konami-twinkle - laser310 - mc10 @@ -19319,6 +20455,7 @@ files: - dragon64 - electron - elektronika-bk + - enterprise-64-128 - fm7 - konami-twinkle - laser310 @@ -19347,6 +20484,7 @@ files: - dragon64 - electron - elektronika-bk + - enterprise-64-128 - fm7 - konami-twinkle - laser310 @@ -19952,6 +21090,7 @@ files: - dragon64 - electron - elektronika-bk + - enterprise-64-128 - konami-twinkle - laser310 - mc10 @@ -19980,6 +21119,7 @@ files: - dragon64 - electron - elektronika-bk + - enterprise-64-128 - konami-twinkle - laser310 - mc10 @@ -20997,6 +22137,7 @@ files: - dragon64 - electron - elektronika-bk + - enterprise-64-128 - fm7 - konami-twinkle - laser310 @@ -21824,6 +22965,7 @@ files: - dragon64 - electron - elektronika-bk + - enterprise-64-128 - fm7 - konami-twinkle - laser310 @@ -21916,6 +23058,7 @@ files: - dragon64 - electron - elektronika-bk + - enterprise-64-128 - konami-twinkle - laser310 - mc10 @@ -22336,6 +23479,7 @@ files: - dragon64 - electron - elektronika-bk + - enterprise-64-128 - fm7 - konami-twinkle - laser310 @@ -22415,6 +23559,7 @@ files: - dragon64 - electron - elektronika-bk + - enterprise-64-128 - fm7 - konami-twinkle - laser310 @@ -24873,6 +26018,7 @@ files: - dragon64 - electron - elektronika-bk + - enterprise-64-128 - konami-twinkle - laser310 - mc10 @@ -24909,6 +26055,7 @@ files: - dragon64 - electron - elektronika-bk + - enterprise-64-128 - konami-twinkle - laser310 - mc10 @@ -25190,6 +26337,7 @@ files: - dragon64 - electron - elektronika-bk + - enterprise-64-128 - konami-twinkle - laser310 - mc10 @@ -27220,6 +28368,7 @@ files: - dragon64 - electron - elektronika-bk + - enterprise-64-128 - konami-twinkle - laser310 - mc10 @@ -29093,6 +30242,7 @@ files: - dragon64 - electron - elektronika-bk + - enterprise-64-128 - konami-twinkle - laser310 - mc10 @@ -29133,6 +30283,7 @@ files: - dragon64 - electron - elektronika-bk + - enterprise-64-128 - konami-twinkle - laser310 - mc10 diff --git a/scripts/scraper/_hash_merge.py b/scripts/scraper/_hash_merge.py index 9ecb1a96..61757169 100644 --- a/scripts/scraper/_hash_merge.py +++ b/scripts/scraper/_hash_merge.py @@ -21,6 +21,7 @@ from common import PROFILE_BACKUP_SUFFIX, write_text_atomic, yaml_load _MAME_RELEASE_RE = re.compile(r"^0\.\d+") _MAMEDEV = "https://github.com/mamedev/mame" DEVICE_SECTION = " # --- Device ROM sets (generated from MAME -listxml) ---" +SOFTLIST_SECTION = " # --- Software lists (generated from MAME -listxml) ---" _FILES_END = re.compile(r"^\S", re.M) @@ -553,9 +554,9 @@ def _append_new_entries(text: str, files: list[dict], original: str) -> str: lines.append(_format_contents(fe["contents"])) if lines: - # New BIOS root sets go before the generated device section, which - # runs to the end of the files list. - at = _device_section_start(text) + # New BIOS root sets go before the generated sections, which run to + # the end of the files list. + at = generated_start(text) if at is None: text = text.rstrip("\n") + "\n" + "\n".join(lines) + "\n" else: @@ -570,6 +571,18 @@ def _device_section_start(text: str) -> int | None: return match.start() if match else None +def _softlist_section_start(text: str) -> int | None: + """Offset of the generated software list section header, if any.""" + match = re.search(rf"^{re.escape(SOFTLIST_SECTION)}$", text, re.M) + return match.start() if match else None + + +def generated_start(text: str) -> int | None: + """Offset of the first generated section: what a reader wrote ends there.""" + found = [at for at in (_softlist_section_start(text), _device_section_start(text)) if at is not None] + return min(found) if found else None + + def _device_contents(roms: list[dict], old: list[dict]) -> list[dict]: """Contents of a device set, keeping the member descriptions a reader wrote.""" written = {str(c.get("name")): c.get("description") for c in old or []} @@ -641,7 +654,11 @@ def _set_section_systems(text: str, systems: dict[str, list[str]]) -> str: start = _device_section_start(text) if start is None: return text - head, section = text[:start], text[start:] + return text[:start] + _rewrite_systems(text[start:], systems) + + +def _rewrite_systems(section: str, systems: dict[str, list[str]]) -> str: + """Rewrite the system fields of the named entries of one stretch of text.""" out: list[str] = [] current = "" in_list = False @@ -661,7 +678,7 @@ def _set_section_systems(text: str, systems: dict[str, list[str]]) -> str: out.append(line) if current in systems and re.match(r"^ category:", line): out.extend(_system_lines(systems[current])) - return head + "\n".join(out) + return "\n".join(out) def _format_device_entry(name: str, device: dict, role: str) -> str: @@ -762,6 +779,118 @@ def merge_mame_devices( return report +def _softlist_section(profile: dict, lists: dict[str, list[str]], role: str) -> str: + """The generated software list section, one entry per list and tree. + + A list takes the fields of each hash/ tree the profile delivers (a + bundled tree ships with the emulator, not in a pack) and the path of + that tree. Members the section names leave the tree's own entry. + """ + trees = [ + entry for entry in profile.get("files") or [] + if entry.get("type") == "directory" + and str(entry.get("name")) == "hash/" + and not entry.get("bundled") + ] + blocks: list[str] = [] + for tree in trees: + base = str(tree.get("path") or tree["name"]) + for name, systems in sorted(lists.items()): + lines = [ + f" - name: {_yaml_scalar(name + '.xml')}", + f" path: {_yaml_scalar(base + name + '.xml')}", + ] + if tree.get("mode"): + lines.append(f" mode: {tree['mode']}") + lines += [ + " category: game_data", + " required: false", + *_system_lines(systems), + ] + blocks.append("\n".join(lines)) + if not blocks: + return "" + lead = [ + SOFTLIST_SECTION, + " # software_list_device::parse opens .xml for each software list a", + " # machine declares. Each list below is filed under the systems whose", + " # machines declare it, a list no such machine declares stays with its", + " # hash/ tree.", + f" # ref: {role}src/emu/softlist_dev.cpp:281-313,", + f" # {role}src/frontend/mame/infoxml.cpp:2184-2198", + ] + return "\n".join(lead) + "\n\n" + "\n\n".join(blocks) + "\n" + + +def merge_mame_support( + profile_path: str, + lists: dict[str, list[str]], + samples: dict[str, list[str]], + write: bool = False, +) -> dict[str, list[str]]: + """File the software lists and the floppy samples of a MAME profile. + + *lists* maps a software list to the systems whose machines declare it, + for the lists the release ships. They are written as the generated + software list section, ahead of the device section, and a refresh + rewrites the whole section. *samples* maps a floppy sample name to the + systems whose drives load it: each written entry named .wav + under a samples/floppy/ path gets them, and one no system loads gets + none. + """ + path = Path(profile_path) + text = path.read_text(encoding="utf-8") + profile = yaml_load(text) or {} + role = mame_ref_role(profile) + + original = text + start = _softlist_section_start(text) + if start is not None: + device = _device_section_start(text) + if device is not None and device > start: + end = device + else: + end_match = _FILES_END.search(text, text.index("\n", start) + 1) + end = end_match.start() if end_match else len(text) + text = text[:start] + text[end:] + section = _softlist_section(profile, lists, role) + + head_end = generated_start(text) + head = text if head_end is None else text[:head_end] + written = yaml_load(head) or {} + sample_systems: dict[str, list[str]] = {} + for entry in written.get("files") or []: + name = str(entry.get("name") or "") + if "samples/floppy/" not in str(entry.get("path") or "") or not name.endswith(".wav"): + continue + wanted = samples.get(name[:-4], []) + if _entry_systems(entry) != wanted: + sample_systems[name] = wanted + head = _rewrite_systems(head, sample_systems) if sample_systems else head + rest = "" if head_end is None else text[head_end:] + + if section and not rest: + files_at = re.search(r"^files:[ \t]*$", head, re.M) + if files_at is None or _FILES_END.search(head, files_at.end()): + raise ValueError( + "files: must be the last top-level key to hold a software list section" + ) + patched = head.rstrip("\n") + "\n" + if section: + patched += "\n" + section + if rest: + patched += "\n" + rest + + if write and patched != original: + shutil.copy2(path, path.with_suffix(PROFILE_BACKUP_SUFFIX)) + write_text_atomic(str(path), patched) + return { + "lists": sorted(lists), + "samples": sorted(sample_systems), + "changed": [str(path)] if patched != original else [], + } + + def _add_source_refs(text: str, refs: dict[str, str]) -> str: """Give each named entry a source_ref line, right after its category.""" if not refs: diff --git a/scripts/scraper/mame_devices.py b/scripts/scraper/mame_devices.py index 618e7004..6fb41c68 100644 --- a/scripts/scraper/mame_devices.py +++ b/scripts/scraper/mame_devices.py @@ -10,7 +10,10 @@ at the release tag. A set whose block cannot be told apart is left without lines and reported. The same document says which machines load each set, so a set is filed under -the systems of those machines (attribute_systems). +the systems of those machines (attribute_systems). It also names the software +lists each machine declares and the floppy drives it can instantiate, which +files hash/.xml (attribute_softlists) and the floppy sound samples +(floppy_sample_systems) the same way. """ from __future__ import annotations @@ -19,7 +22,7 @@ import html import io import re import zipfile -from collections.abc import Callable, Iterator +from collections.abc import Callable, Collection, Iterable, Iterator from concurrent.futures import ThreadPoolExecutor from contextlib import contextmanager from pathlib import Path @@ -27,7 +30,7 @@ from typing import NamedTuple from common import yaml_load -from ._hash_merge import _device_section_start +from ._hash_merge import generated_start from .mame_parser import _collect_rom_macros, _expand_macros, _parse_rom_entries LISTXML_CHUNK = 8 * 1024 * 1024 @@ -40,6 +43,7 @@ _BIOSSET = re.compile(rb"]*)>") _ROM = re.compile(rb"]*)>") _DEVICE_REF = re.compile(rb']*\bname="([^"]+)"') _SLOT_OPTION = re.compile(rb']*\bdevname="([^"]+)"') +_SOFTWARE_LIST = re.compile(rb']*\bname="([^"]+)"') _ROM_START = re.compile(r"ROM_START\s*\(\s*(\w+)\s*\)") _ROM_END = re.compile(r"\bROM_END\b") @@ -112,12 +116,15 @@ class MachineLinks(NamedTuple): *romof* is the parent the element names. *uses* are the devices it can instantiate: every subdevice of its default configuration, each device a selectable slot option adds, and for a device its parent ROM device, - whose set the device's ROMs are also searched in. + whose set the device's ROMs are also searched in. *softlists* are the + software lists the element declares, *sourcefile* the file defining it. """ romof: str isdevice: bool uses: tuple[str, ...] + softlists: tuple[str, ...] = () + sourcefile: str = "" class Release(NamedTuple): @@ -134,8 +141,9 @@ def scan_release(stream: io.BufferedIOBase) -> Release: driver's parent (src/frontend/mame/infoxml.cpp:761-765) or a device's parent ROM device (856-858), device_ref every subdevice of the default configuration (786, 1010-1015), slotoption each selectable card of a - slot (2129-2172). device_t::searchpath puts the parent ROM device after - the device itself (src/emu/device.cpp:140-152). + slot (2129-2172), softwarelist each software list device of the + configuration (2184-2198). device_t::searchpath puts the parent ROM + device after the device itself (src/emu/device.cpp:140-152). """ tag = "" devices: dict[str, dict] = {} @@ -162,7 +170,11 @@ def scan_release(stream: io.BufferedIOBase) -> Release: if isdevice and romof: uses.add(romof) uses.discard(name) - machines[name] = MachineLinks(romof, isdevice, tuple(sorted(uses))) + softlists = {found.decode() for found in _SOFTWARE_LIST.findall(body)} + machines[name] = MachineLinks( + romof, isdevice, tuple(sorted(uses)), tuple(sorted(softlists)), + attrs.get("sourcefile", ""), + ) if isdevice: roms = _device_roms(body) if roms: @@ -187,13 +199,14 @@ def scan_device_sets(stream: io.BufferedIOBase) -> tuple[str, dict[str, dict]]: def profile_seeds(text: str) -> dict[str, str]: - """Set name to system, for the bios_zip entries above the device section. + """Set name to system, for the bios_zip entries before the generated ones. These are the entries a reader wrote, the seeds attribute_systems starts - from. The generated section is left out: its systems derive from these, - and reading them back would carry a previous release's answer forward. + from. The generated sections are left out: their systems derive from + these, and reading them back would carry a previous release's answer + forward. """ - start = _device_section_start(text) + start = generated_start(text) written = yaml_load(text if start is None else text[:start]) or {} seeds: dict[str, str] = {} for entry in written.get("files") or []: @@ -259,6 +272,157 @@ def attribute_systems( return {name: sorted(found) for name, found in systems.items()} +def platform_seeds( + stated: Iterable[tuple[str, str]], + machines: dict[str, MachineLinks], + systems: Collection[str], + written: dict[str, str], +) -> tuple[dict[str, str], dict[str, list[str]]]: + """Seeds a platform file states, and the sets it states two ways. + + *stated* are (archive name, system) pairs a platform declares. A pair + seeds a set when the archive is a machine of the release, not a device, + the system is one the profile lists, and the profile has not written + the set itself. A set stated under two of the profile's systems seeds + nothing and is returned with them. + """ + found: dict[str, set[str]] = {} + listed = set(systems) + for archive, system in stated: + if not archive.endswith(".zip") or system not in listed: + continue + name = archive[:-4] + links = machines.get(name) + if name in written or links is None or links.isdevice: + continue + found.setdefault(name, set()).add(system) + seeds = {name: next(iter(s)) for name, s in found.items() if len(s) == 1} + conflicts = {name: sorted(s) for name, s in found.items() if len(s) > 1} + return seeds, conflicts + + +def attribute_softlists( + machines: dict[str, MachineLinks], placed: dict[str, list[str]] +) -> dict[str, list[str]]: + """Systems whose machines declare each software list. + + *placed* is what attribute_systems returns. A list goes to the systems + of every element declaring it: a machine writes the lists of its whole + configuration and a slot card its own (infoxml.cpp:2184-2198). + software_list_device::parse opens .xml from the hash path + (src/emu/softlist_dev.cpp:281-313). + """ + lists: dict[str, set[str]] = {} + for name, systems in placed.items(): + links = machines.get(name) + for listed in links.softlists if links else (): + lists.setdefault(listed, set()).update(systems) + return {name: sorted(found) for name, found in lists.items()} + + +# floppy_image_device adds its sound device under this shortname +# (src/devices/imagedev/floppy.cpp:50, 2076-2082). +FLOPPY_SOUND = "flopsnd" + +_FORM_FACTOR = re.compile(r"\bm_form_factor\s*=\s*floppy_image::(FF_\w+)") +_SAMPLE_FORM_FACTOR = re.compile( + r"\bm_default_samples\.set_form_factor\s*\(\s*floppy_image::(FF_\w+)" + r"|\bm_default_samples\.add_\w+_sample\s*\(\s*\"([^\"]+)\"" +) + + +def default_floppy_samples(text: str) -> dict[str, str]: + """Each sample of floppy_sound_device's default list, to its form factor. + + The constructor sets a form factor, then adds the samples recorded for + it (src/devices/imagedev/floppy.cpp:1656-1686). + """ + body = re.search( + r"floppy_sound_device::floppy_sound_device\s*\([^)]*\)[^{]*\{(.*?)\n\}", + blank_comments(text), + re.S, + ) + samples: dict[str, str] = {} + current = "" + for match in _SAMPLE_FORM_FACTOR.finditer(body.group(1) if body else ""): + if match.group(1): + current = match.group(1) + elif current: + samples[match.group(2)] = current + return samples + + +def drive_form_factor(text: str, shortname: str) -> str: + """Form factor a floppy drive type sets in setup_characteristics, or "".""" + text = blank_comments(text) + declaration = re.search( + r"DEFINE_DEVICE_TYPE\w*\s*\(\s*\w+\s*,\s*(\w+)\s*,\s*\"" + re.escape(shortname) + '"', + text, + ) + if declaration is None: + return "" + body = re.search( + rf"\b{re.escape(declaration.group(1))}::setup_characteristics\s*\(\s*\)\s*\{{(.*?)\n\}}", + text, + re.S, + ) + found = _FORM_FACTOR.search(body.group(1)) if body else None + return found.group(1) if found else "" + + +def selected_form_factor(form_factor: str, available: Collection[str]) -> str: + """The list floppy_sound_samples::select takes for a drive's form factor. + + 3" falls back to 3.5", anything else to 5.25", and a form factor no list + answers takes none (src/devices/imagedev/floppy.cpp:1428-1473). + """ + while form_factor not in available: + if form_factor == "FF_3": + form_factor = "FF_35" + elif form_factor != "FF_525": + form_factor = "FF_525" + else: + return "" + return form_factor + + +def floppy_drives(machines: dict[str, MachineLinks]) -> list[str]: + """Device types that instantiate the floppy sound device.""" + return sorted( + name for name, links in machines.items() + if links.isdevice and FLOPPY_SOUND in links.uses + ) + + +def floppy_sample_systems( + machines: dict[str, MachineLinks], + placed: dict[str, list[str]], + samples: dict[str, str], + form_factors: dict[str, str], +) -> dict[str, list[str]]: + """Systems whose machines load each floppy sound sample. + + Every drive starts a floppy sound device, which loads the default list + of the form factor its drive selects (floppy.cpp:1709-1738, + 1759-1767). *samples* maps a sample to its form factor + (default_floppy_samples), *form_factors* a drive to its own + (drive_form_factor). A drive whose form factor is unknown is taken to + load every list. + """ + available = set(samples.values()) + found: dict[str, set[str]] = {} + for drive in floppy_drives(machines): + systems = placed.get(drive) + if not systems: + continue + own = form_factors.get(drive, "") + chosen = {selected_form_factor(own, available)} if own else available + for sample, form_factor in samples.items(): + if form_factor in chosen: + found.setdefault(sample, set()).update(systems) + return {name: sorted(s) for name, s in found.items()} + + def source_path(sourcefile: str) -> str: """Repository path of a listxml sourcefile. diff --git a/scripts/scraper/mame_hash_scraper.py b/scripts/scraper/mame_hash_scraper.py index d3e7dec6..3e21ccad 100644 --- a/scripts/scraper/mame_hash_scraper.py +++ b/scripts/scraper/mame_hash_scraper.py @@ -23,7 +23,14 @@ from typing import Any import yaml -from common import github_token, source_yaml_paths, write_text_atomic, yaml_load +from common import ( + github_token, + list_registered_platforms, + load_platform_config, + source_yaml_paths, + write_text_atomic, + yaml_load, +) import upstream @@ -33,13 +40,21 @@ from ._hash_merge import ( compute_diff, merge_mame_devices, merge_mame_profile, + merge_mame_support, ) from .mame_devices import ( + attribute_softlists, attribute_systems, + default_floppy_samples, + drive_form_factor, + floppy_drives, + floppy_sample_systems, locate_sources, open_listxml, + platform_seeds, profile_seeds, scan_release, + source_path, ) from .mame_parser import parse_mame_source_tree @@ -51,6 +66,8 @@ _CACHE_PATH = _ROOT / "data" / "mame-hashes.json" # release by a second run while the first was still parsing it. _CLONE_DIR = _ROOT / "tmp" / f"mame-{os.getpid()}" _EMULATORS_DIR = _ROOT / "emulators" +_PLATFORMS_DIR = _ROOT / "platforms" +_FLOPPY_SOURCE = "src/devices/imagedev/floppy.cpp" _REPO_URL = "https://github.com/mamedev/mame.git" _STALE_HOURS = 24 @@ -474,6 +491,17 @@ def _write_release(args: argparse.Namespace) -> None: print(f"{args.profile}: {len(profile['files'])} BIOS root sets at {args.release}") +def _platform_archives() -> list[tuple[str, str]]: + """(file name, system) for every file a registered platform declares.""" + stated: list[tuple[str, str]] = [] + for name in list_registered_platforms(str(_PLATFORMS_DIR), include_archived=True): + config = load_platform_config(name, str(_PLATFORMS_DIR)) + for system, data in (config.get("systems") or {}).items(): + for entry in (data or {}).get("files") or []: + stated.append((str(entry.get("name") or ""), str(system))) + return stated + + def _merge_devices(args: argparse.Namespace) -> None: """Document the device ROM sets of the release mame.yml is pinned on. @@ -498,15 +526,39 @@ def _merge_devices(args: argparse.Namespace) -> None: f"{tag} is {commit}, mame.yml upstream_commit is " f"{profile.get('upstream_commit')}" ) - unplaced = locate_sources( - devices, - lambda path: upstream.fetch_file(repo, commit, path, upstream.CACHE_DIR), + + def fetch(path: str) -> list[str] | None: + return upstream.fetch_file(repo, commit, path, upstream.CACHE_DIR) + + unplaced = locate_sources(devices, fetch) + written = profile_seeds(text) + stated, conflicts = platform_seeds( + _platform_archives(), machines, profile.get("systems") or [], written ) - systems = attribute_systems(machines, profile_seeds(text)) + systems = attribute_systems(machines, {**stated, **written}) for set_name, device in devices.items(): device["systems"] = systems.get(set_name, []) report = merge_mame_devices(str(profile_path), devices, write=not args.dry_run) + shipped = upstream.list_directory(repo, commit, "hash", upstream.CACHE_DIR) + if shipped is None: + raise RuntimeError(f"hash/ of {repo.slug} at {commit} could not be listed") + present = {name[:-4] for name in shipped if name.endswith(".xml")} + declared = attribute_softlists(machines, systems) + lists = {name: found for name, found in declared.items() if name in present} + floppy = fetch(_FLOPPY_SOURCE) + if floppy is None: + raise RuntimeError(f"{_FLOPPY_SOURCE} is absent at {commit}") + samples = default_floppy_samples("\n".join(floppy)) + form_factors: dict[str, str] = {} + for drive in floppy_drives(machines): + lines = fetch(source_path(machines[drive].sourcefile)) + form_factors[drive] = drive_form_factor("\n".join(lines or []), drive) + sample_systems = floppy_sample_systems(machines, systems, samples, form_factors) + support = merge_mame_support( + str(profile_path), lists, sample_systems, write=not args.dry_run + ) + print(f"mame-devices: {len(devices)} device ROM sets in the {tag} listxml") print(f" {profile_path.name}:") print(f" + {len(report['added'])} added") @@ -518,9 +570,27 @@ def _merge_devices(args: argparse.Namespace) -> None: print( f" systems: {by_count[1]} sets under one system, " f"{sum(n for k, n in by_count.items() if k > 1)} under several, " - f"{by_count[0]} loaded by no machine a profile entry places " + f"{by_count[0]} loaded by no placed machine " "(they serve every system of the profile)" ) + print( + f" seeds: {len(written)} written in {profile_path.name}, " + f"{len(stated)} from platform files" + ) + for set_name, found in sorted(conflicts.items()): + print(f" {set_name}.zip stated under {', '.join(found)}, left unseeded") + print( + f" software lists: {len(lists)} filed under their systems, " + f"{len(present) - len(lists)} declared by no placed machine stay with hash/, " + f"{len(set(declared) - present)} placed lists absent from hash/" + ) + unread = sorted(drive for drive, found in form_factors.items() if not found) + print( + f" floppy samples: {len(sample_systems)} of {len(samples)} filed, " + f"{len(support['samples'])} entries rewritten" + ) + for drive in unread: + print(f" {drive}: no form factor read, taken to load every list") if unplaced: print(f" {len(unplaced)} without source lines:") for set_name, reason in sorted(unplaced.items()): diff --git a/scripts/upstream.py b/scripts/upstream.py index 025b1de2..69fdae40 100644 --- a/scripts/upstream.py +++ b/scripts/upstream.py @@ -713,6 +713,28 @@ def _tree_url(repo: Repo, sha: str) -> str | None: return f"{repo.api_base}/repos/{repo.slug}/git/trees/{sha}?recursive=1" +def list_directory( + repo: Repo, sha: str, path: str, cache_dir: str, offline: bool = False +) -> list[str] | None: + """Names of the blobs directly under one directory at one revision. + + One request for the directory alone, where the whole tree of a large + repository comes back truncated. None when the forge has no such call + or the directory does not exist there. + """ + if repo.family != "github": + return None + url = f"{repo.api_base}/repos/{repo.slug}/git/trees/{sha}:{path.strip('/')}" + payload = _api(url, cache_dir, offline) + if not isinstance(payload, dict) or payload.get("truncated"): + return None + return sorted( + entry["path"] + for entry in payload.get("tree") or [] + if isinstance(entry, dict) and entry.get("type") == "blob" and entry.get("path") + ) + + TREE_PAGE_SIZE = 100 MAX_TREE_PAGES = 50 diff --git a/tests/test_mame_devices.py b/tests/test_mame_devices.py index 2e679f98..ff00c52c 100644 --- a/tests/test_mame_devices.py +++ b/tests/test_mame_devices.py @@ -18,22 +18,31 @@ from pathlib import Path from scripts.common import yaml_load from scripts.scraper._hash_merge import ( DEVICE_SECTION, + SOFTLIST_SECTION, mame_ref_role, merge_mame_devices, merge_mame_profile, + merge_mame_support, ) from scripts.scraper.mame_devices import ( ABSENT, AMBIGUOUS, NO_FILE, MachineLinks, + attribute_softlists, attribute_systems, + default_floppy_samples, + drive_form_factor, find_rom_block, + floppy_drives, + floppy_sample_systems, locate_sources, open_listxml, + platform_seeds, profile_seeds, scan_device_sets, scan_release, + selected_form_factor, source_path, ) @@ -429,7 +438,10 @@ class TheListxmlSaysWhoLoadsASet(unittest.TestCase): def test_links(self): machines = _release().machines - self.assertEqual(machines["mslugj"], MachineLinks("mslug", False, ("cart_dev",))) + self.assertEqual( + machines["mslugj"], + MachineLinks("mslug", False, ("cart_dev",), (), "snk/neogeo.cpp"), + ) self.assertEqual(machines["apple2gs"].uses, ("a2diskii", "a2ssc")) # A device's ROMs are searched in its parent ROM device's set too. self.assertEqual(machines["cart_dev"].uses, ("cart_base",)) @@ -633,6 +645,333 @@ class APickReadsTheEntrySystems(unittest.TestCase): ) +SUPPORT_LISTXML = b""" + +\t +\t\t +\t\t +\t +\t +\t\t +\t +\t +\t\t +\t\t\t +\t\t +\t +\t +\t\t +\t\t +\t +\t +\t\t +\t +\t +\t\t +\t\t +\t +\t +\t\t +\t +\t +\t\t +\t +\t +\t\t +\t +\t +\t\t +\t +\t +\t + +""" + +SUPPORT_SEEDS = {"bbcb": "acorn-bbc-micro", "apple2gs": "apple2gs", "pcw": "pcw"} + +FLOPPY_CPP = """\ +floppy_sound_device::floppy_sound_device(const machine_config &mconfig, const char *tag) +\t: samples_device(mconfig, FLOPPYSOUND, tag, owner, clock), +\tm_spin_kind(floppy_sound_samples::QUIET) +{ +\t// m_default_samples.add_spin_sample("commented_out", 0); +\tm_default_samples.set_form_factor(floppy_image::FF_35, "floppy"); +\tm_default_samples.add_spin_sample("35_spin", floppy_sound_samples::START_EMPTY); +\tm_default_samples.add_seek_sample("35_seek", 2, 3); +\tm_default_samples.set_form_factor(floppy_image::FF_525, "floppy"); +\tm_default_samples.add_step_sample("525_step"); +} + +DEFINE_DEVICE_TYPE(FLOPPY_35_DD, floppy_35_dd, "floppy_35_dd", "3.5 inch drive") +DEFINE_DEVICE_TYPE(FLOPPY_525_QD, floppy_525_qd, "floppy_525_qd", "5.25 inch drive") +DEFINE_DEVICE_TYPE(FLOPPY_3_SSSD, floppy_3_sssd, "floppy_3_sssd", "3 inch drive") +DEFINE_DEVICE_TYPE(FLOPPY_8_SSSD, floppy_8_sssd, "floppy_8_sssd", "8 inch drive") + +void floppy_35_dd::setup_characteristics() +{ +\tm_form_factor = floppy_image::FF_35; +} + +void floppy_525_qd::setup_characteristics() +{ +\tm_form_factor = floppy_image::FF_525; +} + +void floppy_3_sssd::setup_characteristics() +{ +\tm_form_factor = floppy_image::FF_3; +} + +void floppy_8_sssd::setup_characteristics() +{ +\tm_form_factor = floppy_image::FF_8; +} +""" + + +def _support(): + machines = scan_release(io.BytesIO(SUPPORT_LISTXML)).machines + return machines, attribute_systems(machines, SUPPORT_SEEDS) + + +class SoftwareListsFollowTheMachinesThatDeclareThem(unittest.TestCase): + """software_list_device::parse opens .xml for each list a machine + declares: a --system pick needs the lists of that system's machines.""" + + def test_the_scan_keeps_lists_and_source_files(self): + machines = scan_release(io.BytesIO(SUPPORT_LISTXML)).machines + self.assertEqual(machines["bbcb"].softlists, ("bbc_flop",)) + self.assertEqual(machines["a2disk35"].sourcefile, "devices/bus/a2bus/a2disk35.cpp") + + def test_lists_of_machines_clones_and_slot_cards(self): + lists = attribute_softlists(*_support()) + self.assertEqual(lists["bbc_flop"], ["acorn-bbc-micro"]) + self.assertEqual(lists["bbc_flop_us"], ["acorn-bbc-micro"]) + self.assertEqual(lists["apple2gs_flop"], ["apple2gs"]) + # No entry places the PC: its list serves no system of the profile. + self.assertNotIn("ibm5150", lists) + + +class FloppySamplesFollowTheDrives(unittest.TestCase): + """Every drive adds a floppy sound device that loads the default list of + the form factor its drive selects (floppy.cpp:1428-1473, 1709-1738).""" + + def test_default_list_by_form_factor(self): + self.assertEqual( + default_floppy_samples(FLOPPY_CPP), + {"35_spin": "FF_35", "35_seek": "FF_35", "525_step": "FF_525"}, + ) + + def test_drive_form_factor(self): + self.assertEqual(drive_form_factor(FLOPPY_CPP, "floppy_35_dd"), "FF_35") + self.assertEqual(drive_form_factor(FLOPPY_CPP, "floppy_3_sssd"), "FF_3") + self.assertEqual(drive_form_factor(FLOPPY_CPP, "missing"), "") + + def test_select_falls_back_like_the_code(self): + available = {"FF_35", "FF_525"} + self.assertEqual(selected_form_factor("FF_35", available), "FF_35") + self.assertEqual(selected_form_factor("FF_3", available), "FF_35") + self.assertEqual(selected_form_factor("FF_8", available), "FF_525") + self.assertEqual(selected_form_factor("FF_TWIG", available), "FF_525") + self.assertEqual(selected_form_factor("FF_3", {"FF_525"}), "FF_525") + self.assertEqual(selected_form_factor("FF_35", set()), "") + + def test_samples_go_to_the_systems_of_the_drives(self): + machines, systems = _support() + self.assertEqual( + floppy_drives(machines), + ["floppy_35_dd", "floppy_3_sssd", "floppy_525_qd", "floppy_8_sssd"], + ) + samples = default_floppy_samples(FLOPPY_CPP) + form_factors = {d: drive_form_factor(FLOPPY_CPP, d) for d in floppy_drives(machines)} + found = floppy_sample_systems(machines, systems, samples, form_factors) + self.assertEqual(found["35_spin"], ["apple2gs", "pcw"]) + self.assertEqual(found["525_step"], ["acorn-bbc-micro"]) + + def test_an_unread_form_factor_loads_every_list(self): + machines, systems = _support() + samples = default_floppy_samples(FLOPPY_CPP) + found = floppy_sample_systems(machines, systems, samples, {}) + self.assertEqual(found["525_step"], ["acorn-bbc-micro", "apple2gs", "pcw"]) + + +class PlatformFilesSeedMachines(unittest.TestCase): + + def test_a_platform_statement_seeds_a_machine(self): + machines = scan_release(io.BytesIO(SUPPORT_LISTXML)).machines + seeds, conflicts = platform_seeds( + [ + ("bbcb.zip", "acorn-bbc-micro"), + ("bbcb.zip", "bbcmicro"), # not a system of the profile + ("ibm5150.zip", "ibm-pc"), # idem + ("a2disk35.zip", "apple2gs"), # a device, not a machine + ("pcw.zip", "pcw"), # written by the profile + ("apple2gs.zip", "apple2gs"), + ("apple2gs.zip", "acorn-bbc-micro"), + ("missing.zip", "pcw"), + ("bbcb.rom", "acorn-bbc-micro"), + ], + machines, + ["acorn-bbc-micro", "apple2gs", "pcw"], + {"pcw": "pcw"}, + ) + self.assertEqual(seeds, {"bbcb": "acorn-bbc-micro"}) + self.assertEqual(conflicts, {"apple2gs": ["acorn-bbc-micro", "apple2gs"]}) + + +SUPPORT_PROFILE = """emulator: MAME +source: "https://github.com/libretro/mame" +upstream: "https://github.com/mamedev/mame" +systems: + - acorn-bbc-micro + - apple2gs +files: + - name: "hash/" + path: "mame/hash/" + type: directory + mode: libretro + category: game_data + required: false + + - name: "hash/" + path: "hash/" + type: directory + mode: standalone + category: game_data + required: false + bundled: true + + - name: "35_spin.wav" + path: "mame/samples/floppy/35_spin.wav" + mode: libretro + category: game_data + required: false + size: 4 + + - name: "525_step.wav" + path: "mame/samples/floppy/525_step.wav" + mode: libretro + category: game_data + system: pcw + required: false + + - name: neogeo.zip + required: true + category: bios_zip + system: acorn-bbc-micro +""" + + +class TheSupportMergeWritesListsAndSamples(unittest.TestCase): + + def setUp(self): + self._tmp = tempfile.TemporaryDirectory() + self.path = Path(self._tmp.name) / "mame.yml" + self.path.write_text( + SUPPORT_PROFILE + "\n" + DEVICE_SECTION + "\n\n - name: ym2610.zip\n" + " required: false\n category: bios_zip\n", + encoding="utf-8", + ) + self.lists = {"bbc_flop": ["acorn-bbc-micro"], "apple2gs_flop": ["acorn-bbc-micro", "apple2gs"]} + self.samples = {"35_spin": ["apple2gs"]} + + def tearDown(self): + self._tmp.cleanup() + + def _merge(self) -> dict: + return merge_mame_support(str(self.path), self.lists, self.samples, write=True) + + def _entries(self) -> list[dict]: + return yaml_load(self.path.read_text(encoding="utf-8"))["files"] + + def test_one_entry_per_list_under_the_delivered_tree(self): + self._merge() + text = self.path.read_text(encoding="utf-8") + self.assertLess(text.index(SOFTLIST_SECTION), text.index(DEVICE_SECTION)) + lists = [e for e in self._entries() if str(e["name"]).endswith(".xml")] + self.assertEqual( + [(e["name"], e["mode"], e.get("system"), e.get("systems")) for e in lists], + [ + ("apple2gs_flop.xml", "libretro", None, ["acorn-bbc-micro", "apple2gs"]), + ("bbc_flop.xml", "libretro", "acorn-bbc-micro", None), + ], + ) + self.assertFalse(any(str(e.get("path")).startswith("hash/") and e["name"] != "hash/" + for e in self._entries())) + self.assertIn("upstream src/emu/softlist_dev.cpp:281-313", text) + + def test_samples_get_the_systems_of_their_drives(self): + self._merge() + entries = {e["name"]: e for e in self._entries()} + self.assertEqual(entries["35_spin.wav"]["system"], "apple2gs") + self.assertEqual(entries["35_spin.wav"]["size"], 4) + # No drive of a profile system loads it any more: it serves them all. + self.assertNotIn("system", entries["525_step.wav"]) + self.assertEqual(entries["neogeo.zip"]["system"], "acorn-bbc-micro") + + def test_a_refresh_rewrites_the_section(self): + self._merge() + self.lists = {"bbc_flop": ["apple2gs"]} + self._merge() + names = [e["name"] for e in self._entries()] + self.assertNotIn("apple2gs_flop.xml", names) + self.assertEqual(names.count("bbc_flop.xml"), 1) + self.assertEqual(names[-1], "ym2610.zip") + self.lists = {} + self._merge() + self.assertNotIn(SOFTLIST_SECTION, self.path.read_text(encoding="utf-8")) + + def test_a_second_run_changes_nothing(self): + self._merge() + first = self.path.read_text(encoding="utf-8") + report = self._merge() + self.assertEqual(report["changed"], []) + self.assertEqual(report["samples"], []) + self.assertEqual(self.path.read_text(encoding="utf-8"), first) + + def test_new_bios_roots_and_seeds_stay_ahead_of_the_sections(self): + self._merge() + text = self.path.read_text(encoding="utf-8") + self.assertEqual(profile_seeds(text), {"neogeo": "acorn-bbc-micro"}) + + def test_a_pick_takes_its_lists_and_leaves_the_others(self): + from scripts.common import entry_serves_systems, expand_directory_entries + + self._merge() + profile = yaml_load(self.path.read_text(encoding="utf-8")) + db = {"files": {}, "indexes": {"by_path_suffix": {}}} + for index, dest in enumerate( + ["mame/hash/bbc_flop.xml", "mame/hash/apple2gs_flop.xml", "mame/hash/nes.xml"] + ): + sha1 = f"{index:040d}" + db["files"][sha1] = {"path": f"bios/Other/{dest}", "name": dest.rsplit("/", 1)[1]} + db["indexes"]["by_path_suffix"][dest] = [sha1] + files = [e for e in profile["files"] if e.get("mode") != "standalone"] + files = expand_directory_entries(files, db) + picked = sorted( + str(e.get("path")) + for e in files + if str(e.get("path", "")).startswith("mame/hash/") + and entry_serves_systems(e, profile, ["apple2gs"]) + ) + self.assertEqual(picked, ["mame/hash/apple2gs_flop.xml", "mame/hash/nes.xml"]) + + +class TheRepositoryProfile(unittest.TestCase): + """mame.yml as committed: every list filed is one the hash tree holds.""" + + def test_generated_lists_sit_inside_the_hash_tree(self): + path = Path(__file__).resolve().parents[1] / "emulators" / "mame.yml" + text = path.read_text(encoding="utf-8") + start = text.find(SOFTLIST_SECTION) + self.assertNotEqual(start, -1) + section = yaml_load("files:\n" + text[start:text.index(DEVICE_SECTION)])["files"] + self.assertTrue(section) + for entry in section: + self.assertEqual(entry["path"], "mame/hash/" + entry["name"]) + self.assertTrue(entry.get("system") or entry.get("systems"), entry["name"]) + + class Cli(unittest.TestCase): def test_devices_needs_a_listxml(self): diff --git a/tests/test_upstream.py b/tests/test_upstream.py index c4f7302a..f4c5f536 100644 --- a/tests/test_upstream.py +++ b/tests/test_upstream.py @@ -598,6 +598,36 @@ class TestCompare(unittest.TestCase): upstream._http_json, upstream._http_text = self._orig self.tmp.cleanup() + def test_list_directory_reads_one_tree(self): + seen: list[str] = [] + + def answer(url): + seen.append(url) + return { + "truncated": False, + "tree": [ + {"path": "b.xml", "type": "blob"}, + {"path": "a.xml", "type": "blob"}, + {"path": "sub", "type": "tree"}, + ], + } + + upstream._http_json = answer + self.assertEqual( + upstream.list_directory(self.repo, "abc", "hash/", self.dir), ["a.xml", "b.xml"] + ) + self.assertTrue(seen[0].endswith("/repos/o/n/git/trees/abc:hash")) + # Cached: offline, the same answer without a request. + self.assertEqual( + upstream.list_directory(self.repo, "abc", "hash", self.dir, offline=True), + ["a.xml", "b.xml"], + ) + self.assertEqual(len(seen), 1) + upstream._http_json = lambda url: {"truncated": True, "tree": []} + self.assertIsNone(upstream.list_directory(self.repo, "def", "hash", self.dir)) + gitlab = parse_repo("https://gitlab.com/o/n") + self.assertIsNone(upstream.list_directory(gitlab, "abc", "hash", self.dir)) + def test_renamed_file_carries_previous_path(self): self.payload = { "files": [ diff --git a/wiki/testing-guide.md b/wiki/testing-guide.md index 52742fce..275dda99 100644 --- a/wiki/testing-guide.md +++ b/wiki/testing-guide.md @@ -117,6 +117,17 @@ and merged into a profile, on an inline listxml and driver source. Covers: `systems:` written and rewritten by a refresh, a written entry's system kept, and a `--system` pick reading the entry's own systems through the gap report and the core extras +- Seeds a platform file states: a machine of the release under one of the + profile's systems, a device, an unlisted system or a set stated two ways + seeding nothing +- Support files: each software list filed under the systems whose machines + and slot cards declare it, one entry per list of the delivered `hash/` + tree, a pick keeping its lists and the unfiled ones, floppy samples filed + through the form factor each drive selects (3" falls back to 3.5", the + rest to 5.25"), a refresh rewriting the section and a second run changing + nothing +- An entry's `system:` or `systems:` outside its profile's list refused by + the semantic check **test_fbneo_parser.** Tests the FBNeo C source parser that identifies `BDF_BOARDROM` sets. Same inline fixture approach. @@ -150,8 +161,8 @@ replaced by an injected fetch function. **test_upstream.** Covers forge URL parsing for GitHub, GitLab and Forgejo, the content-addressed cache and its atomic write, revision and tag resolution, -and tree comparison. The HTTP layer is replaced at module level, so nothing -leaves the machine. +tree comparison and the listing of one directory. The HTTP layer is replaced at +module level, so nothing leaves the machine. ### Tests that read the working tree diff --git a/wiki/tools.md b/wiki/tools.md index e4bc8967..00bfe665 100644 --- a/wiki/tools.md +++ b/wiki/tools.md @@ -877,6 +877,23 @@ their parent ROM devices. A set loaded by machines of one system gets system of the profile. The driver file is never used: `neogeo.cpp` declares the MVS and the AES. The output counts the sets in each case. +A machine set no written entry lists is also seeded when a registered platform +declares `.zip` under one of the profile's systems (Batocera files +`bbcb.zip` under `acorn-bbc-micro`). The set must be a machine of the release, +not a device, and a set two platforms state under two different systems seeds +nothing and is listed. + +The same walk files the support trees. Each `hash/.xml` the release +ships and a placed machine or slot card declares (`softwarelist`) is written +under the `Software lists (generated from MAME -listxml)` comment, ahead of the +device section, with the systems of those machines. A list no placed machine +declares stays a member of the `hash/` entry. Each floppy sound sample gets the +systems whose machines can instantiate a drive selecting its list: the form +factor comes from the drive's `setup_characteristics` in the source, 3" drives +take the 3.5" list and every other drive the 5.25" one, as +`floppy_sound_samples::select` does. `plugins/` stays unfiled: every machine +starts the Lua engine. + A plain run (pipeline step 2a) that moves `mame.yml` to a new release downloads that release's `-listxml` into `.cache/dats/` and runs the device merge itself, so the BIOS root sets and the device sets name one release.