mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-13 05:23:51 -06:00
fix: place files in a folder a launcher sets
KNULLI sets AetherSX2's [Folders] Bios to bios/ps2 (aethersx2Generator.py:219), yet its 112 PS2 BIOS files went to the BIOS root. Three causes: - a record's passes_to was split into words, so "[Folders] Bios" could not be told from "[Filenames] BIOS". ini_keys() adds the section-qualified key a profile names in standalone_directories. - scans_dir records hand a folder too (ROCKNIX's bios/armsx2, EmuDeck's PCSX2 and ARMSX2) and were not read as handed. - the agnostic scan looked its seeds up by the id of the profile's entries, while it walks the copies platform_layouts makes: any bound layout scanned nothing. Copies map back to the profile's entries by position. An agnostic profile's entry the layout reads outside the BIOS tree is now named by the gap report (outside_bios_folder) instead of dropped: ARMSX2's arcade/proverb.zip on ROCKNIX and EmuDeck.
This commit is contained in:
1 parent
62278e1d4e
commit
ddc947c85d
4 files changed
+192
-10
No files matched your search
+18
-3
@@ -312,8 +312,10 @@ def launcher_places(config: dict) -> list[LauncherPlace]:
|
||||
# component scripts $bios_path.
|
||||
BIOS_ROOT_VARIABLES = ("$biosdir", "$home/RetroPie/BIOS", "${biosPath}", "$bios_path")
|
||||
|
||||
# The kinds that hand an emulator a whole directory.
|
||||
HANDED_KINDS = ("links", "passes_path")
|
||||
# The kinds that hand an emulator a whole directory. A scans_dir record sets
|
||||
# the directory the emulator picks its file from: ROCKNIX writes ARMSX2's
|
||||
# [Folders] Bios, EmuDeck PCSX2's.
|
||||
HANDED_KINDS = ("links", "passes_path", "scans_dir")
|
||||
|
||||
# How a launcher record says the platform starts the emulator in a directory
|
||||
# (retropie_launchers._working_directory), and the handle a profile gives
|
||||
@@ -357,6 +359,19 @@ def normalize_handle(token: str) -> str:
|
||||
return _FLATPAK_DATA.sub("$XDG_DATA_HOME", token)
|
||||
|
||||
|
||||
_INI_KEY = re.compile(r"(\[[^\]\s]+\])\s+([^\s,;=()]+)")
|
||||
|
||||
|
||||
def ini_keys(passes_to: str) -> frozenset[str]:
|
||||
"""The INI keys a record's ``passes_to`` writes, as "[Section] Key".
|
||||
|
||||
A key alone is ambiguous: PCSX2 reads its BIOS folder from [Folders]
|
||||
Bios and the image name from [Filenames] BIOS. A profile declares the
|
||||
key it resolves a folder from in that spelling ("[Folders] Bios = bios").
|
||||
"""
|
||||
return frozenset(f"{section} {key}" for section, key in _INI_KEY.findall(passes_to))
|
||||
|
||||
|
||||
def _folder_in_bios(directory: str, base: str) -> str | None:
|
||||
"""A directory relative to the BIOS root, None when it lies outside."""
|
||||
relative = _bios_relative(directory, base)
|
||||
@@ -485,7 +500,7 @@ def handed_directories(config: dict) -> list[HandedDirectory]:
|
||||
handle
|
||||
for token in passes_to.split()
|
||||
if (handle := normalize_handle(token)) and handle != directory
|
||||
)
|
||||
) | ini_keys(passes_to)
|
||||
folder = _folder_in_bios(part, base)
|
||||
for emulator in (e.strip() for e in str(record.get("emulator", "")).split(",")):
|
||||
found.append(HandedDirectory(
|
||||
|
||||
+15
-4
@@ -567,7 +567,15 @@ def _agnostic_scan_extras(
|
||||
agnostic_votes.get(directory + "/", 0) + 1
|
||||
)
|
||||
|
||||
for profile, standalone in platform_layouts(emu_name, profile, standalone_set or set()):
|
||||
own = profile
|
||||
for profile, standalone in platform_layouts(emu_name, own, standalone_set or set()):
|
||||
# A layout copies each entry in order (a launcher's folder, a
|
||||
# system directory): its files are looked up as the profile's
|
||||
# own, or a bound layout scans nothing (KNULLI's bios/ps2).
|
||||
origin = {
|
||||
id(view): entry
|
||||
for view, entry in zip(profile.get("files", []), own.get("files", []))
|
||||
}
|
||||
for f in profile.get("files", []):
|
||||
if not is_profile_agnostic and not f.get("agnostic"):
|
||||
continue
|
||||
@@ -617,8 +625,8 @@ def _agnostic_scan_extras(
|
||||
companion_ext, image = _companion_of(profile, fname)
|
||||
|
||||
path_prefix = (
|
||||
resolved_dirs.get(id(image), "") if image
|
||||
else resolved_dirs.get(id(f), "")
|
||||
resolved_dirs.get(id(origin.get(id(image), image)), "") if image
|
||||
else resolved_dirs.get(id(origin.get(id(f), f)), "")
|
||||
)
|
||||
# A placeholder (<bios>.bin) resolves to nothing and anchors no
|
||||
# directory. The folders where the system's other profiles
|
||||
@@ -639,7 +647,10 @@ def _agnostic_scan_extras(
|
||||
# A name matching one file identifies it. A name matching several
|
||||
# identifies nothing on its own, so the profile's other files have
|
||||
# to agree on the directory before the scan walks it.
|
||||
ambiguous = id(f) in named_only and len(by_name.get(fname, [])) > 1
|
||||
ambiguous = (
|
||||
id(origin.get(id(f), f)) in named_only
|
||||
and len(by_name.get(fname, [])) > 1
|
||||
)
|
||||
if ambiguous and agnostic_votes.get(path_prefix, 0) < 2:
|
||||
continue
|
||||
|
||||
|
||||
+7
-3
@@ -564,15 +564,19 @@ def find_undeclared_files(
|
||||
if emu_name not in relevant:
|
||||
continue
|
||||
|
||||
# Skip agnostic profiles entirely (filename-agnostic BIOS detection)
|
||||
if profile.get("bios_mode") == "agnostic":
|
||||
continue
|
||||
# A filename-agnostic profile is served by the scan (packextras), all
|
||||
# but the entries its layout reads outside the BIOS tree: those are
|
||||
# named here, or a launcher folder drops them without a word
|
||||
# (ARMSX2's arcade/proverb.zip once ROCKNIX binds bios/armsx2).
|
||||
agnostic = profile.get("bios_mode") == "agnostic"
|
||||
|
||||
for profile, is_standalone in platform_layouts(emu_name, profile, standalone_set):
|
||||
|
||||
for f in expand_directory_entries(
|
||||
profile.get("files", []), db, is_standalone
|
||||
):
|
||||
if agnostic and not f.get("unplaced"):
|
||||
continue
|
||||
fname = f.get("name", "")
|
||||
# One destination rule for the dedup key and for the entry: a
|
||||
# standalone build without standalone_path falls back on path.
|
||||
|
||||
@@ -553,5 +553,157 @@ class EachProgramItsOwnProfile(unittest.TestCase):
|
||||
self.assertEqual(resolve_platform_cores(config, profiles), {"emu"})
|
||||
|
||||
|
||||
BIOS_SETTING = {
|
||||
"emulator": "ps2emu", "build": "standalone", "kind": "passes_path", "names": [],
|
||||
"search_dir": "/userdata/bios/ps2",
|
||||
"passes_to": "inis/PCSX2.ini [Folders] Bios = ../../../bios/ps2, relative to the data root",
|
||||
"source_ref": "ps2emuGenerator.py:219",
|
||||
}
|
||||
# The same folder through a scans_dir record, and a record setting the image
|
||||
# name alone, which hands no folder.
|
||||
BIOS_SCAN = {**BIOS_SETTING, "kind": "scans_dir", "passes_to": "PCSX2.ini [Folders] Bios"}
|
||||
BIOS_NAME = {**BIOS_SETTING, "passes_to": "PCSX2.ini [Filenames] BIOS"}
|
||||
|
||||
|
||||
class ASettingSetsTheBiosFolder(unittest.TestCase):
|
||||
"""KNULLI sets AetherSX2's [Folders] Bios to bios/ps2.
|
||||
|
||||
The profile reads its BIOS images by content from bios/ under its data
|
||||
root, and names that setting in standalone_directories. Every image the
|
||||
scan finds goes to ps2/ in the pack, the manifest and verify, and a file
|
||||
the layout keeps outside its bios/ folder is named, never packed.
|
||||
"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
_emulator_profiles_cache.clear()
|
||||
(REPO_ROOT / "tmp").mkdir(exist_ok=True)
|
||||
self.root = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
|
||||
self.addCleanup(shutil.rmtree, self.root, True)
|
||||
files, by_md5, by_name = {}, {}, {}
|
||||
for folder, name, data in (
|
||||
("Sony/PlayStation 2", "img-a.bin", b"PS2 IMAGE A....."),
|
||||
("Sony/PlayStation 2", "img-b.bin", b"PS2 IMAGE B....."),
|
||||
("Other/ps2emu/arcade", "boot.zip", b"BOOT PROGRAMS"),
|
||||
):
|
||||
where = self.root / "bios" / folder
|
||||
where.mkdir(parents=True, exist_ok=True)
|
||||
(where / name).write_bytes(data)
|
||||
sha1 = hashlib.sha1(data).hexdigest()
|
||||
md5 = hashlib.md5(data).hexdigest()
|
||||
files[sha1] = {"path": str(where / name), "name": name, "sha1": sha1,
|
||||
"md5": md5, "crc32": "", "size": len(data)}
|
||||
by_md5[md5] = sha1
|
||||
by_name[name] = [sha1]
|
||||
self.db = {
|
||||
"files": files,
|
||||
"indexes": {"by_md5": by_md5, "by_name": by_name, "by_crc32": {},
|
||||
"by_path_suffix": {}},
|
||||
}
|
||||
self.platforms = self.root / "platforms"
|
||||
self.emulators = self.root / "emulators"
|
||||
self.platforms.mkdir()
|
||||
self.emulators.mkdir()
|
||||
platforms = {"setplat": [BIOS_SETTING], "scanplat": [BIOS_SCAN],
|
||||
"nameplat": [BIOS_NAME], "plainplat": []}
|
||||
for name, launchers in platforms.items():
|
||||
(self.platforms / f"{name}.yml").write_text(yaml.safe_dump({
|
||||
"platform": name, "version": "1", "verification_mode": "existence",
|
||||
"base_destination": "bios", "cores": ["ps2emu"],
|
||||
"standalone_cores": ["ps2emu"], "launchers": launchers,
|
||||
"systems": {"sony-playstation-2": {"files": []}},
|
||||
}))
|
||||
(self.platforms / "_registry.yml").write_text(yaml.safe_dump({"platforms": {
|
||||
name: {"config": f"{name}.yml"} for name in platforms
|
||||
}}))
|
||||
(self.emulators / "ps2emu.yml").write_text(yaml.safe_dump({
|
||||
"emulator": "PS2Emu", "type": "standalone", "bios_mode": "agnostic",
|
||||
"cores": ["ps2emu"], "systems": ["sony-playstation-2"],
|
||||
"bios_directory": "bios/",
|
||||
"standalone_directories": ["[Folders] Bios = bios"],
|
||||
"files": [
|
||||
{"name": "img-a.bin", "path": "bios/img-a.bin", "required": True,
|
||||
"min_size": 16, "max_size": 16, "validation": ["size"]},
|
||||
# Read below the data root, never below its bios/ folder.
|
||||
{"name": "boot.zip", "standalone_path": "arcade/boot.zip",
|
||||
"mode": "standalone", "required": False},
|
||||
],
|
||||
}))
|
||||
|
||||
def pack_names(self, platform: str) -> list[str]:
|
||||
with redirect_stdout(io.StringIO()):
|
||||
zip_path = generate_pack(
|
||||
platform, str(self.platforms), self.db, str(self.root / "bios"),
|
||||
str(self.root / "out" / platform), emulators_dir=str(self.emulators),
|
||||
offline=True,
|
||||
)
|
||||
with zipfile.ZipFile(zip_path) as zf:
|
||||
return sorted(n for n in zf.namelist() if n.endswith((".bin", ".zip")))
|
||||
|
||||
def manifest(self, platform: str) -> dict:
|
||||
with redirect_stdout(io.StringIO()):
|
||||
return generate_manifest(
|
||||
platform, str(self.platforms), self.db, str(self.root / "bios"),
|
||||
str(self.platforms / "_registry.yml"),
|
||||
emulators_dir=str(self.emulators), offline=True,
|
||||
)
|
||||
|
||||
def verify_dests(self, platform: str) -> list[str]:
|
||||
config = load_platform_config(platform, str(self.platforms))
|
||||
result = verify_platform(config, self.db, str(self.emulators))
|
||||
return sorted(d for d in result["pack_destinations"] if d.endswith((".bin", ".zip")))
|
||||
|
||||
def assert_placed(self, platform: str, expected: list[str]) -> None:
|
||||
self.assertEqual(self.pack_names(platform), expected)
|
||||
self.assertEqual(
|
||||
sorted(f["dest"] for f in self.manifest(platform)["files"]), expected
|
||||
)
|
||||
self.assertEqual(self.verify_dests(platform), expected)
|
||||
|
||||
def test_every_scanned_image_lands_in_the_folder_set(self) -> None:
|
||||
self.assert_placed("setplat", ["ps2/img-a.bin", "ps2/img-b.bin"])
|
||||
|
||||
def test_a_scans_dir_record_sets_it_too(self) -> None:
|
||||
self.assert_placed("scanplat", ["ps2/img-a.bin", "ps2/img-b.bin"])
|
||||
|
||||
def test_the_image_name_setting_hands_no_folder(self) -> None:
|
||||
self.assert_placed("nameplat", ["img-a.bin", "img-b.bin"])
|
||||
|
||||
def test_a_platform_without_the_record_keeps_the_layout(self) -> None:
|
||||
self.assert_placed("plainplat", ["img-a.bin", "img-b.bin"])
|
||||
|
||||
def test_a_file_outside_the_bios_folder_is_named(self) -> None:
|
||||
omitted = [o for o in self.manifest("setplat")["omitted_files"]
|
||||
if o["name"] == "boot.zip"]
|
||||
self.assertEqual(
|
||||
[(o["dest"], o["root"], o["reason"], o["cores"]) for o in omitted],
|
||||
[("arcade/boot.zip", "emulator", "outside_bios_folder", ["ps2emu"])],
|
||||
)
|
||||
|
||||
def test_the_ini_key_carries_its_section(self) -> None:
|
||||
from launchers import ini_keys
|
||||
|
||||
self.assertEqual(
|
||||
ini_keys("PCSX2.ini [Folders] Bios = /storage/roms/bios/armsx2, [Filenames] BIOS"),
|
||||
{"[Folders] Bios", "[Filenames] BIOS"},
|
||||
)
|
||||
self.assertEqual(ini_keys("command line -rompath"), frozenset())
|
||||
|
||||
|
||||
class AetherSX2OnKnulli(unittest.TestCase):
|
||||
"""The real profile against KNULLI's aethersx2Generator.py:219 record."""
|
||||
|
||||
def test_the_bios_images_land_in_ps2(self) -> None:
|
||||
profile = load_emulator_profiles(str(REPO_ROOT / "emulators"))["aethersx2"]
|
||||
config = load_platform_config("knulli", str(REPO_ROOT / "platforms"))
|
||||
(view, standalone), = platform_layouts("aethersx2", profile, standalone_set(config))
|
||||
self.assertTrue(standalone)
|
||||
for entry in view["files"]:
|
||||
path = entry.get("path") or ""
|
||||
if path.startswith("bios/"):
|
||||
self.assertEqual(entry["standalone_path"], "ps2/" + path.removeprefix("bios/"))
|
||||
elif path:
|
||||
self.assertEqual(entry.get("unplaced"), "outside_bios_folder", path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user