Skip to content

Commit 5cffcfc

Browse files
committed
rework mess support for mame/lr-mame
This is a rework for old computers support in mame (mess). The new code is using lua autoboot scripts to load floppy && cass (with fast cass loading when possible). I have used https://github.com/dsync89/mess-curated-autoboot-scripts as base, but I have reworked a lot of scripts and write new one to handle auto-loading based on cassette headers or floppy disk structure, without relying of specific rom name. The lua code is shared between mame && lr-mame, so it's remove a lot of spaghetti code in both generator. It's re-introduce a fake "mess" core, to cleary split code between softlist (mess) && arcade/rom (mame), and avoid the need to rely on system.name. (And also allow user to add new custom systems, compute or arcade, without need to hack the generatorsà In mess mode, by default, a sha1 of the rom is done to compare to mame xml files, to detect the correct softlist and configure the machine && media. A new messSoftlistMap.json has been added, with all existing mess softlists and best compatible machine && media. It's possible to define custom options for each softmap in the json too. (We could also add a new "MESS" system if needed, to allow user to put all obscures system games inside, it should work automatically if autodetected) If no auto detection is found, it's search the default mapping from the system name. For this, A new messSystems.json is introduce to replace messSystems.csv, with mapping based on rom extension to a mess softlist.
1 parent 561369a commit 5cffcfc

88 files changed

Lines changed: 24123 additions & 1259 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

package/batocera/core/batocera-configgen/configgen/configgen/generators/libretro/libretroGenerator.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,6 @@ def getHotkeysContext(self) -> HotkeysContext:
5656
# Main entry of the module
5757
# Configure retroarch and return a command
5858
def generate(self, system, rom, playersControllers, metadata, guns, wheels, gameResolution):
59-
# Fix for the removed MESS/MAMEVirtual cores
60-
if system.config.core in [ 'mess', 'mamevirtual' ]:
61-
system.config['core'] = 'mame'
6259

6360
# Get the graphics backend first
6461
gfxBackend = getGFXBackend(system)
@@ -126,11 +123,13 @@ def generate(self, system, rom, playersControllers, metadata, guns, wheels, game
126123
shutil.copyfile(RETROARCH_CUSTOM, remapconfigDir / "common.rmp")
127124

128125
# Retroarch core on the filesystem
129-
retroarchCore = RETROARCH_CORES / f"{system.config.core}_libretro.so"
126+
# 'mess' uses the mame core binary
127+
coreBinary = "mame" if system.config.core == "mess" else system.config.core
128+
retroarchCore = RETROARCH_CORES / f"{coreBinary}_libretro.so"
130129

131130
# for each core, a file /usr/lib/<core>.info must exit, otherwise, info such as rewinding/netplay will not work
132131
# to do a global check : cd /usr/lib/libretro && for i in *.so; do INF=$(echo $i | sed -e s+/usr/lib/libretro+/usr/share/libretro/info+ -e s+\.so+.info+); test -e "$INF" || echo $i; done
133-
infoFile = RETROARCH_SHARE / "info" / f"{system.config.core}_libretro.info"
132+
infoFile = RETROARCH_SHARE / "info" / f"{coreBinary}_libretro.info"
134133
if not infoFile.exists():
135134
raise MissingCore
136135

package/batocera/core/batocera-configgen/configgen/configgen/generators/libretro/libretroMAMEConfig.py

Lines changed: 206 additions & 403 deletions
Large diffs are not rendered by default.

package/batocera/core/batocera-configgen/configgen/configgen/generators/mame/mameGenerator.py

Lines changed: 366 additions & 583 deletions
Large diffs are not rendered by default.

package/batocera/core/batocera-configgen/configgen/configgen/generators/mame/mamePaths.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,10 @@
1010
MAME_CHEATS: Final = CHEATS / "mame"
1111
MAME_ROMS: Final = ROMS / "mame"
1212
MAME_DEFAULT_DATA: Final = DEFAULTS_DIR / "data" / "mame"
13+
14+
MESS_ROMS: Final = ROMS / "mess"
15+
MESS_SOFTLIST_MAP: Final = MAME_DEFAULT_DATA / "messSoftlistMap.json"
16+
MESS_SYSTEMS_MAPPING: Final = MAME_DEFAULT_DATA / "messSystems.json"
17+
MESS_AUTOBOOT_SCRIPTS: Final = MAME_DEFAULT_DATA / "autoboot_scripts"
18+
MESS_HASH_CACHE: Final = MAME_SAVES / "mess_hash_cache.json"
19+
MAME_HASH_DIR: Final = MAME_BIOS / "hash"
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
from __future__ import annotations
2+
3+
import hashlib
4+
import json
5+
import logging
6+
import re
7+
import time
8+
import xml.etree.ElementTree as ET
9+
import zipfile
10+
from pathlib import Path
11+
from typing import TypedDict
12+
13+
from ...batoceraPaths import mkdir_if_not_exists
14+
from .mamePaths import (
15+
MAME_HASH_DIR,
16+
MESS_HASH_CACHE,
17+
MESS_SOFTLIST_MAP,
18+
)
19+
20+
_logger = logging.getLogger(__name__)
21+
22+
_MAME_BUILTIN_HASH_DIR = Path("/usr/bin/mame/hash")
23+
_CACHE_VERSION = 1
24+
25+
26+
class RomInfo(TypedDict):
27+
softlist: str
28+
software: str
29+
media: str
30+
31+
32+
# cdrom must precede cd so that _cdrom is not partially matched as _cd
33+
_SOFTLIST_SUFFIX_RE = re.compile(
34+
r'_(cdrom|flop|cart|cass|disk|cd|rom|snap|hdd|quik|qd|cyl|ptp|ctape|card|ssd).*$'
35+
)
36+
37+
38+
def _machine_from_softlist(softlist: str) -> str:
39+
"""Derive a machine name from a softlist name by stripping trailing media suffixes."""
40+
return _SOFTLIST_SUFFIX_RE.sub('', softlist)
41+
42+
43+
def _lookup_rom(sha1: str) -> RomInfo | None:
44+
"""Look up a SHA1 in the MAME software-list cache, building it if needed."""
45+
cache = _load_or_build_hash_cache()
46+
return cache.get(sha1)
47+
48+
49+
def _build_config_args(config_args: list[dict], system, machine: str) -> list[str]:
50+
"""
51+
Evaluate a config_args spec from messSoftlistMap.json against the live system
52+
config and return the resulting MAME command-line tokens.
53+
"""
54+
result: list[str] = []
55+
for item in config_args:
56+
key = item["key"]
57+
kind = item.get("type", "str")
58+
skip_if = item.get("skip_if")
59+
60+
only = item.get("only_machines")
61+
if only is not None and machine not in only:
62+
continue
63+
64+
overrides: dict = item.get("machine_overrides", {}).get(machine, {})
65+
66+
if kind == "bool":
67+
value = system.config.get_bool(key, item.get("default", False))
68+
if value:
69+
result += item.get("if_true", [])
70+
else:
71+
result += item.get("if_false", [])
72+
73+
elif kind == "int":
74+
value = system.config.get_int(key) or 0
75+
if skip_if is not None and value == skip_if:
76+
continue
77+
if value:
78+
value = overrides.get("value_map", {}).get(str(value), value)
79+
if "max" in overrides:
80+
value = min(value, overrides["max"])
81+
result += [s.replace("{value}", str(value)) for s in item.get("args", [])]
82+
83+
else: # str
84+
value = system.config.get_str(key) or item.get("default", "")
85+
if skip_if is not None and value == skip_if:
86+
continue
87+
if value:
88+
restriction = item.get("value_machine_restriction", {})
89+
if value in restriction and machine not in restriction[value]:
90+
_logger.debug(
91+
"MESS: skipping -%s %s: not supported on machine %s (allowed: %s)",
92+
key, value, machine, restriction[value],
93+
)
94+
continue
95+
result += [s.replace("{value}", value) for s in item.get("args", [])]
96+
97+
return result
98+
99+
100+
def _build_rom_ext_args(rom_ext_args: list[dict], rom: Path) -> list[str]:
101+
"""Emit static args based on the ROM file extension."""
102+
ext = rom.suffix.lower()
103+
result: list[str] = []
104+
for item in rom_ext_args:
105+
if ext in item.get("extensions", []):
106+
result += item.get("args", [])
107+
return result
108+
109+
110+
def _load_softlist_map() -> dict:
111+
"""Load the softlist→machine/media/autoboot mapping JSON."""
112+
if not MESS_SOFTLIST_MAP.exists():
113+
_logger.warning("MESS: messSoftlistMap.json not found at %s", MESS_SOFTLIST_MAP)
114+
return {}
115+
try:
116+
with MESS_SOFTLIST_MAP.open() as f:
117+
data = json.load(f)
118+
data.pop("_comment", None)
119+
return data
120+
except (json.JSONDecodeError, OSError) as exc:
121+
_logger.error("MESS: Failed to load messSoftlistMap.json: %s", exc)
122+
return {}
123+
124+
125+
def _compute_sha1(rom: Path) -> str:
126+
"""
127+
Compute the SHA1 of the ROM's raw data.
128+
129+
For .zip archives, hashes the first file inside (as MAME stores sha1 of the
130+
uncompressed content, not the archive itself).
131+
"""
132+
suffix = rom.suffix.casefold()
133+
134+
if suffix == ".zip":
135+
try:
136+
with zipfile.ZipFile(rom, "r") as zf:
137+
names = [n for n in zf.namelist() if not n.endswith("/")]
138+
if names:
139+
with zf.open(names[0]) as inner:
140+
return _sha1_of_stream(inner)
141+
except (zipfile.BadZipFile, KeyError, OSError) as exc:
142+
_logger.warning("MESS: Could not read zip %s: %s – hashing archive directly", rom, exc)
143+
144+
with rom.open("rb") as f:
145+
return _sha1_of_stream(f)
146+
147+
148+
def _sha1_of_stream(stream) -> str:
149+
h = hashlib.sha1()
150+
for chunk in iter(lambda: stream.read(1 << 20), b""):
151+
h.update(chunk)
152+
return h.hexdigest()
153+
154+
155+
def _get_hash_dirs() -> list[Path]:
156+
"""Return candidate MAME hash directories, user-supplied first."""
157+
dirs = []
158+
if MAME_HASH_DIR.exists():
159+
dirs.append(MAME_HASH_DIR)
160+
if _MAME_BUILTIN_HASH_DIR.exists():
161+
dirs.append(_MAME_BUILTIN_HASH_DIR)
162+
return dirs
163+
164+
165+
def _hash_dir_mtime() -> float:
166+
"""Return the most recent mtime across all hash directories."""
167+
mtime = 0.0
168+
for d in _get_hash_dirs():
169+
try:
170+
mtime = max(mtime, d.stat().st_mtime)
171+
except OSError:
172+
pass
173+
return mtime
174+
175+
176+
def _load_or_build_hash_cache() -> dict[str, RomInfo]:
177+
"""Load the SHA1→RomInfo cache from disk, rebuilding if stale."""
178+
if MESS_HASH_CACHE.exists():
179+
try:
180+
with MESS_HASH_CACHE.open() as f:
181+
data = json.load(f)
182+
if (
183+
data.get("_version") == _CACHE_VERSION
184+
and data.get("_mtime", 0) >= _hash_dir_mtime()
185+
):
186+
return data.get("entries", {})
187+
except (json.JSONDecodeError, OSError):
188+
pass
189+
190+
_logger.info("MESS: Building software-list hash cache (this may take a moment)…")
191+
t0 = time.monotonic()
192+
entries = _build_hash_index()
193+
elapsed = time.monotonic() - t0
194+
_logger.info("MESS: Cache built with %d entries in %.1fs", len(entries), elapsed)
195+
196+
payload = {
197+
"_version": _CACHE_VERSION,
198+
"_mtime": _hash_dir_mtime(),
199+
"entries": entries,
200+
}
201+
try:
202+
mkdir_if_not_exists(MESS_HASH_CACHE.parent)
203+
tmp = MESS_HASH_CACHE.with_suffix(".tmp")
204+
with tmp.open("w") as f:
205+
json.dump(payload, f)
206+
tmp.replace(MESS_HASH_CACHE)
207+
except OSError as exc:
208+
_logger.warning("MESS: Could not write hash cache: %s", exc)
209+
210+
return entries
211+
212+
213+
def _build_hash_index() -> dict[str, RomInfo]:
214+
"""Parse all MAME software-list XMLs and build a sha1→RomInfo mapping."""
215+
index: dict[str, RomInfo] = {}
216+
217+
for hash_dir in _get_hash_dirs():
218+
for xml_file in sorted(hash_dir.glob("*.xml")):
219+
softlist_name = xml_file.stem
220+
try:
221+
context = ET.iterparse(xml_file, events=("start",))
222+
current_software: str = ""
223+
current_media: str = ""
224+
for _, elem in context:
225+
tag = elem.tag
226+
if tag == "software":
227+
current_software = elem.get("name", "")
228+
elif tag == "part":
229+
current_media = elem.get("name", "")
230+
elif tag == "rom":
231+
sha1 = (elem.get("sha1") or "").lower()
232+
if sha1 and current_software and sha1 not in index:
233+
index[sha1] = RomInfo(
234+
softlist=softlist_name,
235+
software=current_software,
236+
media=current_media,
237+
)
238+
except ET.ParseError as exc:
239+
_logger.warning("MESS: Skipping malformed XML %s: %s", xml_file, exc)
240+
241+
return index

package/batocera/core/batocera-configgen/data/mame/atom_flop_autoload.csv

Lines changed: 0 additions & 16 deletions
This file was deleted.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
local button = {}
2+
for key, ports in pairs(manager.machine.ioport.ports) do
3+
for field_name, field in pairs(ports.fields) do
4+
button[field_name] = field
5+
end
6+
end
7+
8+
9+
local frame_num = 0
10+
local function process_frame()
11+
frame_num = frame_num + 1
12+
13+
if frame_num == 1 then
14+
button["CONS.2: Option"]:set_value(1)
15+
end
16+
17+
if frame_num == 300 then
18+
button["CONS.2: Option"]:set_value(0)
19+
end
20+
21+
end
22+
23+
subscription=emu.add_machine_frame_notifier(process_frame)

0 commit comments

Comments
 (0)