fix: read MAME's sets through its search paths

MAME splits rompath, samplepath, hashpath and pluginspath on semicolons
and tries each directory in turn (src/emu/fileio.cpp:53-58, 330-392,
src/emu/emuopts.cpp:44-46, 53). The launcher records spell those lists
with the game's own folder in them, which handed_directories skipped:

- RetroBat passes -rp bios;<rom directory> (Mame64.Generator.cs:93-96)
  and -rompath bios/psxmame/bios;<rom directory> to pSxMAME
  (PSXMame.Generator.cs:38-48).
- KNULLI passes -rompath <romDirname>;/userdata/bios/mame;/userdata/bios
  (mameGenerator.py:116-122).
- RetroPie writes rompath $romdir/mame;$romdir/arcade;$biosdir/mame
  (mame.sh:133).

A semicolon list now gives each directory but the game's folder. A pool
that already lists the folder the files take keeps them there, and a
pool standing for the whole layout holds its files, never its folders.
The MAME profile names its sets at the top of the rompath and its
search settings as pools, pSxMAME its sets and rompath.

RetroBat's -sp and -hash records split on `bool hbmame = emulator ==
"hbmame"` (Mame64.Generator.cs:30): bios/mame/samples for mame64,
bios/hbmame/samples for hbmame.

Manifests: RetroBat gains the 12 pSxMAME sets in psxmame/bios and drops
samples/bbc.zip, read by nobody, which mame/samples/bbc.zip already
serves. KNULLI drops the same copy. RetroPie gains the 1427 MAME sets
in BIOS/mame, the root copies staying for the libretro cores. EmuDeck
names samples/bbc.zip as read outside the BIOS folder. RetroDECK's
samplepath is $bios_path/mame-sa/samples (mame/component_prepare.sh:94,
117): bbc.zip moves there from bios/samples/. Its MAME runs with the
-rompath ES-DE passes, %GAMEDIR%;%ROMPATH%/<system>, so the standalone
sets are named as read outside the BIOS folder and the root copies stay
for the libretro core. The 1427 sets
of RetroBat, KNULLI and EmuDeck stay at the BIOS root, listed in their
rompath. No file of any manifest is lost.
This commit is contained in:
Abdessamad Derraz committed 2026-11-10 13:05:59 +01:00
1 parent 5e13f11e3f
commit 6947df629a
7 files changed
+1665 -22

No files matched your search

+1445
View File
File diff suppressed because it is too large. Load diff
+19
View File
@@ -16,6 +16,10 @@ cores:
- psxmame
systems:
- arcade
standalone_pools:
rompath: ""
"-rompath": ""
"-rp": ""
notes: |
MAME build reduced to the PlayStation based arcade boards, with the ZiNc
@@ -31,6 +35,9 @@ notes: |
zn.c, plus taitogn.c, ksys573.c and konamigv.c. emulatorlauncher runs
`mame.exe -skip_gameinfo -rompath <bios>/psxmame/bios;<game folder>
... <set>` (emulatorLauncher Generators/PSXMame.Generator.cs:20-110).
Each directory of the semicolon-separated rompath is tried in turn and a
set is read at its top (src/emu/emuopts.c:36, src/emu/fileio.c:245-249,
816-835).
A ROM missing from its set is fatal unless it is optional or has no good
dump (src/emu/romload.c:427-449, src/emu/romload.c:594). Of a set with
@@ -40,6 +47,7 @@ notes: |
files:
- name: "cpzn1.zip"
path: "cpzn1.zip"
system: arcade
required: true
category: bios_zip
@@ -52,6 +60,7 @@ files:
sha1: "486d92ff6c7f1e54f8e0ef41cd9116eca0e10e1a"
- name: "cpzn2.zip"
path: "cpzn2.zip"
system: arcade
required: true
category: bios_zip
@@ -64,6 +73,7 @@ files:
sha1: "66e7e1d4e426466b8f48a2ba055a91b475569504"
- name: "atpsx.zip"
path: "atpsx.zip"
system: arcade
required: true
category: bios_zip
@@ -76,6 +86,7 @@ files:
sha1: "815488d8563c85f97fbc3384ff21f08e4c88b7b7"
- name: "acpsx.zip"
path: "acpsx.zip"
system: arcade
required: true
category: bios_zip
@@ -88,6 +99,7 @@ files:
sha1: "7ee83d317190bb1cef2f8f01c81eaaae47150ebb"
- name: "tps.zip"
path: "tps.zip"
system: arcade
required: true
category: bios_zip
@@ -100,6 +112,7 @@ files:
sha1: "03eb2febfab3fcde716defff291babd9392de965"
- name: "vspsx.zip"
path: "vspsx.zip"
system: arcade
required: true
category: bios_zip
@@ -112,6 +125,7 @@ files:
sha1: "8f59314c1093446b9bcb06d232244da6df78e206"
- name: "taitofx1.zip"
path: "taitofx1.zip"
system: arcade
required: true
category: bios_zip
@@ -124,6 +138,7 @@ files:
sha1: "e18907cf8c6ba54d96edba0a9a00487a90219e0d"
- name: "psarc95.zip"
path: "psarc95.zip"
system: arcade
required: true
category: bios_zip
@@ -136,6 +151,7 @@ files:
sha1: "cd68532967a25f476a6d73473ec6b6f4df2e1689"
- name: "atluspsx.zip"
path: "atluspsx.zip"
system: arcade
required: true
category: bios_zip
@@ -148,6 +164,7 @@ files:
sha1: "9511d24bfe25eb180fb2db0835b131cb4a12730e"
- name: "taitogn.zip"
path: "taitogn.zip"
system: arcade
required: true
category: bios_zip
@@ -171,6 +188,7 @@ files:
sha1: "f1b10846a8423d9fe021191c5876190857c3d2a4"
- name: "sys573.zip"
path: "sys573.zip"
system: arcade
required: true
category: bios_zip
@@ -183,6 +201,7 @@ files:
sha1: "e1284add4aaddd5337bd7f4e27614460d52b5b48"
- name: "konamigv.zip"
path: "konamigv.zip"
system: arcade
required: true
category: bios_zip
+26 -6
View File
@@ -4718,16 +4718,26 @@ launchers:
\ == \"hbmame\", !string.IsNullOrEmpty(AppConfig[\"bios\"]) && Directory.Exists(AppConfig[\"\
bios\"])"
source_ref: "emulatorLauncher/Generators/Mame64.Generator.cs:93-96"
- emulator: hbmame
build: standalone
kind: passes_path
names: []
search_dir: bios/hbmame/samples
passes_to: command line -sp bios/hbmame/samples
fails_when_missing: false
depends_on_option: "messMode == null || messMode.Name == \"mame\" || messMode.Name\
\ == \"hbmame\", !string.IsNullOrEmpty(samplePath) && Directory.Exists(samplePath)"
source_ref: "emulatorLauncher/Generators/Mame64.Generator.cs:30, 101, 106-107"
- emulator: mame64
build: standalone
kind: passes_path
names: []
search_dir: bios/hbmame/samples when hbmame, else bios/mame/samples
passes_to: command line -sp bios/hbmame/samples when hbmame, else bios/mame/samples
search_dir: bios/mame/samples
passes_to: command line -sp bios/mame/samples
fails_when_missing: false
depends_on_option: "messMode == null || messMode.Name == \"mame\" || messMode.Name\
\ == \"hbmame\", !string.IsNullOrEmpty(samplePath) && Directory.Exists(samplePath)"
source_ref: "emulatorLauncher/Generators/Mame64.Generator.cs:101, 106-107"
source_ref: "emulatorLauncher/Generators/Mame64.Generator.cs:30, 101, 106-107"
- emulator: mame64
build: standalone
kind: passes_path
@@ -4764,16 +4774,26 @@ launchers:
depends_on_option: "messMode == null || messMode.Name == \"mame\" || messMode.Name\
\ == \"hbmame\", !string.IsNullOrEmpty(iniPath) && Directory.Exists(iniPath)"
source_ref: "emulatorLauncher/Generators/Mame64.Generator.cs:74-76, 167-168"
- emulator: hbmame
build: standalone
kind: passes_path
names: []
search_dir: bios/hbmame/hash
passes_to: command line -hash bios/hbmame/hash
fails_when_missing: false
depends_on_option: "messMode == null || messMode.Name == \"mame\" || messMode.Name\
\ == \"hbmame\", !string.IsNullOrEmpty(hashPath) && Directory.Exists(hashPath)"
source_ref: "emulatorLauncher/Generators/Mame64.Generator.cs:30, 172, 177-178"
- emulator: mame64
build: standalone
kind: passes_path
names: []
search_dir: bios/hbmame/hash when hbmame, else bios/mame/hash
passes_to: command line -hash bios/hbmame/hash when hbmame, else bios/mame/hash
search_dir: bios/mame/hash
passes_to: command line -hash bios/mame/hash
fails_when_missing: false
depends_on_option: "messMode == null || messMode.Name == \"mame\" || messMode.Name\
\ == \"hbmame\", !string.IsNullOrEmpty(hashPath) && Directory.Exists(hashPath)"
source_ref: "emulatorLauncher/Generators/Mame64.Generator.cs:172, 177-178"
source_ref: "emulatorLauncher/Generators/Mame64.Generator.cs:30, 172, 177-178"
- emulator: mame64
build: standalone
kind: passes_path
+45 -10
View File
@@ -341,6 +341,10 @@ INSTALL_TREE = "install"
# A folder of neither tree, which no pack and no installer can fill.
OUTSIDE_TREE = "outside"
Place = tuple[str, str]
# The binding key of a search list standing for the whole layout: the
# directories hold the layout's files themselves (XRoar's ROMs, MAME's sets),
# never its subfolders.
SEARCHED_FILES = "*"
_HOME = re.compile(r"^(?:\$\{HOME\}|\$HOME|\$home|~)(?=/|$)")
# Flatpak sets XDG_DATA_HOME to ~/.var/app/<id>/data inside the sandbox.
@@ -443,13 +447,29 @@ def native_systems(config: dict) -> dict[str, str]:
return found
def _listed_directories(raw: str) -> list[str] | None:
"""The directories of a colon-separated search path, None for one directory.
# How the launcher extractors spell the folder holding the launched game:
# RetroBat's Path.GetDirectoryName(rom) (retrobat_launchers._KNOWN), KNULLI's
# os.path.dirname(rom) and its software list folder (mameGenerator.py:113-116).
GAME_FOLDERS = ("<rom directory>", "<romDirname>", "<softDir>")
XRoar's rompath is such a list (RetroDECK writes four directories), each
entry a path or a variable spelling one. A Windows drive letter or a
backslash means a single path.
def _listed_directories(raw: str) -> list[str] | None:
"""The directories of a search path, None for one directory.
XRoar's rompath is a colon-separated list (RetroDECK writes four
directories), each entry a path or a variable spelling one. A Windows
drive letter or a backslash means a single path. MAME splits its search
paths on semicolons whatever the system (src/emu/fileio.cpp:53-58):
RetroBat passes `-rp bios;<rom directory>`, KNULLI `-rompath
<romDirname>;/userdata/bios/mame;/userdata/bios`. The game's own folder
is no folder a pack fills, so it gives no directory.
"""
if ";" in raw:
parts = [p.strip() for p in raw.split(";") if p.strip()]
kept = [p for p in parts if p not in GAME_FOLDERS]
if kept and all(not re.search(r"[\s,<>]", p) for p in kept):
return kept
return None
if ":" not in raw or "\\" in raw or re.match(r"^[A-Za-z]:", raw):
return None
parts = [p.strip() for p in raw.split(":")]
@@ -722,7 +742,13 @@ def _bind(
one after the other: openMSX's filepool, XRoar's rompath. A file in any
of them is read, so the pool gives one: the directory the platform's own
list already fills (Batocera's bios/openmsx beside bios/Machines), else
the first of the BIOS tree, else the first a pack can fill.
the first of the BIOS tree, else the first a pack can fill. A list that
already holds the folder the layout's files take without the launcher
keeps them there: KNULLI starts MAME with -rompath
<romDirname>;/userdata/bios/mame;/userdata/bios, and the sets at the BIOS
root stay. A pool standing for the whole layout holds its files
directly, not its folders (SEARCHED_FILES): MAME reads hash/ and
samples/ through other settings.
"""
found: dict[str, set[Place]] = {}
pooled: dict[str, list[tuple[bool, Place]]] = {}
@@ -756,11 +782,14 @@ def _bind(
for rest, place in places or [("", (OUTSIDE_TREE, ""))]:
found.setdefault(rest, set()).add(place)
for sub, entries in pooled.items():
# The directory the platform's own list fills, else the first of the
# BIOS tree, else the first a pack fills.
# The folder the files already take, else the directory the
# platform's own list fills, else the first of the BIOS tree, else
# the first a pack fills.
unmoved = [p for _held, p in entries if p == (BIOS_TREE, sub)]
filled = [p for held, p in entries if held]
in_bios = [p for _held, p in entries if p[0] == BIOS_TREE]
found.setdefault(sub, set()).add((filled or in_bios or [p for _h, p in entries])[0])
chosen = (unmoved or filled or in_bios or [p for _h, p in entries])[0]
found.setdefault(sub or SEARCHED_FILES, set()).add(chosen)
return found
@@ -850,9 +879,15 @@ def bound_path(path: str, bindings: dict[str, Place]) -> Place | None:
The longest subfolder that contains the path wins: a link of
kickstarts/ places kickstarts/kick13.rom even when the whole layout is
bound elsewhere.
bound elsewhere. SEARCHED_FILES covers the files at the root of the
layout alone.
"""
for sub in sorted(bindings, key=len, reverse=True):
if sub == SEARCHED_FILES:
if "/" in path:
continue
root, folder = bindings[sub]
return (root, f"{folder}/{path}" if folder else path)
if sub and not path.startswith(sub + "/"):
continue
rest = path[len(sub) + 1:] if sub else path
+41 -1
View File
@@ -3336,7 +3336,11 @@ def mame(sources: dict[str, Source]) -> list[Record]:
raise LauncherPatternMissing(own.path, "MAME folder arguments")
records = _covered(
own,
[_with_conditions(record, conditions) for record, conditions in found],
[
split
for record, conditions in found
for split in _per_emulator(own, _with_conditions(record, conditions))
],
# The bezel artwork folder is read by the launcher, not passed.
skip=(_BIOS_GUARD, r"ConfigureBezels\("),
)
@@ -3424,6 +3428,42 @@ def xroar(sources: dict[str, Source]) -> list[Record]:
return _covered(src, records)
_EMULATOR_BRANCH = re.compile(r"([^\s;<>]+) when (\w+), else ([^\s;<>]+)")
def _per_emulator(src: Source, record: Record) -> list[Record]:
"""A directory chosen by the emulator the generator runs, one record each.
Mame64.Generator.cs sets `bool hbmame = emulator == "hbmame";` and passes
`-sp bios/hbmame/samples` for it, bios/mame/samples for mame64. A choice
on any other condition is left as it reads.
"""
rendered = str(record.get("search_dir", ""))
branch = _EMULATOR_BRANCH.fullmatch(rendered)
if branch is None:
return [record]
chosen, flag, default = branch.groups()
declared = re.search(rf'bool {flag} = emulator == "([\w.-]+)";', _code(src))
if declared is None:
return [record]
ref = str(record["source_ref"])
if not ref.startswith(src.path + ":"):
raise LauncherPatternMissing(src.path, f"source of the directory choice {rendered!r}")
spans = [
(int(first), int(last or first))
for first, _, last in (s.partition("-") for s in ref[len(src.path) + 1:].split(", "))
]
spans.append(src.lines(declared.start(), declared.end()))
prefix = str(record["passes_to"]).removesuffix(rendered).strip()
out = []
for emulator, directory in ((declared.group(1), chosen), (record["emulator"], default)):
split = {k: v for k, v in record.items() if k != "source_ref"}
split.update(emulator=emulator, search_dir=directory, passes_to=f"{prefix} {directory}")
split["source_ref"] = _path_parts(src, *spans)
out.append(split)
return out
_SYSTEM_BRANCH = re.compile(r'(\S+), or (\S+) when system == "([\w.-]+)"')
+52
View File
@@ -807,5 +807,57 @@ class DolphinUserDirectory(unittest.TestCase):
self.assertEqual(ipl.get("unplaced"), "outside_bios_folder")
class MameSearchPaths(unittest.TestCase):
"""MAME reads each semicolon-separated search path in turn
(src/emu/fileio.cpp:53-58, 330-392)."""
def sets(self, view: dict) -> list[dict]:
return [e for e in view["files"] if e.get("category") == "bios_zip"]
def test_retrobat_keeps_the_sets_at_the_bios_root(self) -> None:
# -rp bios;<rom directory> (Mame64.Generator.cs:93-96).
view = standalone_view("mame", "retrobat")
sets = self.sets(view)
self.assertGreater(len(sets), 1000)
for entry in sets:
self.assertEqual(entry["standalone_path"], entry["name"])
self.assertNotIn("unplaced", entry)
(bbc,) = [e for e in view["files"] if e["name"] == "bbc.zip"]
# -sp bios/mame/samples, hbmame's folder being another record
# (Mame64.Generator.cs:30, 101, 106-107).
self.assertEqual(bbc["standalone_path"], "mame/samples/bbc.zip")
def test_knulli_keeps_the_root_listed_after_bios_mame(self) -> None:
# -rompath <romDirname>;/userdata/bios/mame;/userdata/bios (mameGenerator.py:116-122).
view = standalone_view("mame", "knulli")
self.assertTrue(all(e["standalone_path"] == e["name"] for e in self.sets(view)))
(bbc,) = [e for e in view["files"] if e["name"] == "bbc.zip"]
self.assertEqual(bbc["standalone_path"], "mame/samples/bbc.zip")
def test_retropie_moves_the_sets_to_bios_mame(self) -> None:
# rompath $romdir/mame;$romdir/arcade;$biosdir/mame (mame.sh:133).
view = standalone_view("mame", "retropie")
self.assertTrue(all(
e["standalone_path"] == "mame/" + e["name"] for e in self.sets(view)
))
def test_retrodeck_reads_samples_in_mame_sa(self) -> None:
# samplepath $bios_path/mame-sa/samples (mame/component_prepare.sh:94, 117).
view = standalone_view("mame", "retrodeck")
(bbc,) = [e for e in view["files"] if e["name"] == "bbc.zip"]
self.assertEqual(bbc["standalone_path"], "mame-sa/samples/bbc.zip")
# ES-DE hands -rompath %GAMEDIR%;%ROMPATH%/<system>: no BIOS folder.
self.assertTrue(all(
e.get("unplaced") == "outside_bios_folder" for e in self.sets(view)
))
def test_psxmame_reads_bios_psxmame_bios(self) -> None:
# -rompath bios/psxmame/bios;<rom directory> (PSXMame.Generator.cs:38-48).
view = standalone_view("psxmame", "retrobat")
self.assertTrue(view["files"])
for entry in view["files"]:
self.assertEqual(entry["standalone_path"], "psxmame/bios/" + entry["name"])
if __name__ == "__main__":
unittest.main()
+37 -5
View File
@@ -33,6 +33,7 @@ from common import ( # noqa: E402
system_file_index,
)
from launchers import ( # noqa: E402
SEARCHED_FILES,
bound_path,
handed_directories,
normalize_handle,
@@ -205,7 +206,38 @@ class SearchLists(unittest.TestCase):
self.assertEqual(
bind(config(passes("emu", "$roms_path/coco:$bios_path/xroar", "xroar.conf rompath")),
[], {"rompath": ""}),
{"": ("", "xroar")},
{SEARCHED_FILES: ("", "xroar")},
)
def test_a_semicolon_list_leaves_out_the_game_folder(self) -> None:
# MAME splits its search paths on semicolons (src/emu/fileio.cpp:53-58).
found = handed_directories(config(passes(
"emu", "<romDirname>;/userdata/bios/mame;/userdata/bios", "command line -rompath",
)))
self.assertEqual([h.folder for h in found], ["mame", ""])
found = handed_directories(config(passes(
"emu", "bios;<rom directory>", "command line -rp bios;<rom directory>",
)))
self.assertEqual([(h.folder, "-rp" in h.handles) for h in found], [("", True)])
def test_a_list_holding_the_files_folder_keeps_them_there(self) -> None:
# KNULLI lists bios/mame before bios: the sets at the root stay.
cfg = config(passes(
"emu", "<romDirname>;/userdata/bios/mame;/userdata/bios", "command line -rompath",
))
cfg["systems"] = {"x": {"files": [{"name": "a.rom", "destination": "mame/a.rom"}]}}
self.assertEqual(bind(cfg, [], {"-rompath": ""}), {SEARCHED_FILES: ("", "")})
# Without the root, the first folder of the BIOS tree.
cfg = config(passes("emu", "$romdir/mame;$biosdir/mame", "rompath in mame.ini"))
self.assertEqual(bind(cfg, [], {"rompath": ""}), {SEARCHED_FILES: ("", "mame")})
def test_a_whole_layout_list_holds_files_not_folders(self) -> None:
found = {SEARCHED_FILES: ("", "mame")}
self.assertEqual(bound_path("neogeo.zip", found), ("", "mame/neogeo.zip"))
self.assertIsNone(bound_path("samples/bbc.zip", found))
self.assertEqual(
bound_path("samples/bbc.zip", {**found, "samples": ("", "mame-sa/samples")}),
("", "mame-sa/samples/bbc.zip"),
)
def test_a_drive_letter_is_one_directory(self) -> None:
@@ -241,11 +273,11 @@ class PerSystem(unittest.TestCase):
POOLS = {"-rompath": ""}
def test_the_system_record_wins_for_its_system(self) -> None:
self.assertEqual(bind(self.CFG, [], self.POOLS, ("coco",)), {"": ("", "coco")})
self.assertEqual(bind(self.CFG, [], self.POOLS, ("coco",)), {SEARCHED_FILES: ("", "coco")})
def test_another_system_keeps_the_generic_record(self) -> None:
self.assertEqual(bind(self.CFG, [], self.POOLS, ("dragon32",)), {"": ("", "dragon")})
self.assertEqual(bind(self.CFG, [], self.POOLS), {"": ("", "dragon")})
self.assertEqual(bind(self.CFG, [], self.POOLS, ("dragon32",)), {SEARCHED_FILES: ("", "dragon")})
self.assertEqual(bind(self.CFG, [], self.POOLS), {SEARCHED_FILES: ("", "dragon")})
def test_native_names_are_read_as_our_ids(self) -> None:
cfg = {
@@ -270,7 +302,7 @@ class PerSystem(unittest.TestCase):
[("dragon", ["dragon64"]), ("trs80coco", ["coco"])],
)
self.assertEqual(bind(cfg, [], self.POOLS | {"-rompath": ""}, ("coco",)),
{"": ("", "trs80coco")})
{SEARCHED_FILES: ("", "trs80coco")})
self.assertEqual(bind(cfg, [], {"-rompath": ""}, ("dragon32",)), {})
def test_an_entry_is_launched_for_the_system_that_declares_its_name(self) -> None: