diff --git a/platforms/_registry.yml b/platforms/_registry.yml index e9809a0c..90d660fd 100644 --- a/platforms/_registry.yml +++ b/platforms/_registry.yml @@ -499,11 +499,14 @@ platforms: # libretroConfig.py:225-228 and batoceraPaths.py:22 at batocera-43.1). # is the ES system name (Emulator.py:119, args.system), so a # file is placed once below each system es_systems.yml offers the core - # for (libretro_systems in batocera.yml). + # for (libretro_systems in batocera.yml). The pack keeps them in + # userdata-saves/: batocera-systems lists bios/saves/dolphin-emu/ + # Triforce/segaboot.gcm, so saves/ already belongs to the BIOS tree. save_directory: folder: saves by_core: false by_system: true + pack_folder: userdata-saves schedule: weekly target_scraper: batocera_targets target_source: https://github.com/batocera-linux/batocera.linux diff --git a/scripts/generate_pack.py b/scripts/generate_pack.py index 4c47638d..c43bdb81 100644 --- a/scripts/generate_pack.py +++ b/scripts/generate_pack.py @@ -1191,7 +1191,7 @@ def generate_pack( _pack_registry.get(platform_name, {}).get("install", {}) or {} ).get("standalone_copies", []), launch_folders=len(config.get("core_system_dirs") or {}), - save_folder=(save_layout(config).folder if save_count else ""), + save_folder=(save_layout(config).in_pack if save_count else ""), save_where=( save_place( str(_pack_registry.get(platform_name, {}).get("extract_to") or ""), diff --git a/scripts/packextras.py b/scripts/packextras.py index cef99ea4..5895ad7b 100644 --- a/scripts/packextras.py +++ b/scripts/packextras.py @@ -1371,7 +1371,7 @@ def _save_fields(extra: dict, pack_path: str, placement: ExtraPlacement) -> None extra["pack_path"] = pack_path if placement.save is not None: # The path below the save directory, what the manifest writes. - extra["save_dest"] = pack_path[len(placement.save.folder) + 1:] + extra["save_dest"] = pack_path[len(placement.save.in_pack) + 1:] if not placement.save.by_core: extra.pop("core_folder", None) diff --git a/scripts/packpaths.py b/scripts/packpaths.py index 67dcc7d5..3673628e 100644 --- a/scripts/packpaths.py +++ b/scripts/packpaths.py @@ -31,13 +31,23 @@ class SaveLayout: /userdata/saves/): a file read there is placed once for each system that runs the core, the systems the platform file names in ``libretro_systems``. ``by_core_for`` names the cores a launcher - sorts by core where it sorts no other. + sorts by core where it sorts no other. ``pack_folder`` renames the + pack's folder when the BIOS tree holds one of the same name. """ folder: str by_core: bool by_system: bool = False by_core_for: frozenset[str] = frozenset() + pack_folder: str = "" + + @property + def in_pack(self) -> str: + """The pack folder holding these files: ``pack_folder`` where the + platform's own BIOS tree already has a folder named ``folder`` + (Batocera lists bios/saves/dolphin-emu/Triforce/segaboot.gcm), + ``folder`` otherwise.""" + return self.pack_folder or self.folder def sorts(self, names: Iterable[str]) -> bool: """Whether the frontend inserts the core's folder for a core known @@ -64,6 +74,7 @@ def save_layout(config: dict) -> SaveLayout | None: return SaveLayout( folder, bool(block.get("by_core")), bool(block.get("by_system")), frozenset(str(core) for core in block.get("by_core_for") or []), + sanitize_pack_path(str(block.get("pack_folder") or "")), ) @@ -85,7 +96,7 @@ def save_destination( rel = sanitize_pack_path(path) if not rel: return None - root = layout.folder + root = layout.in_pack if layout.by_system: folder = sanitize_pack_path(system) if not folder or "/" in folder: diff --git a/scripts/validate_schemas.py b/scripts/validate_schemas.py index 7c1c89f2..165f71d1 100644 --- a/scripts/validate_schemas.py +++ b/scripts/validate_schemas.py @@ -416,8 +416,10 @@ def _save_directory_blocks() -> list[str]: if ( not isinstance(block, dict) or not {"folder", "by_core"} <= set(block) <= { - "folder", "by_core", "by_system", "by_core_for", + "folder", "by_core", "by_system", "by_core_for", "pack_folder", } + or not isinstance(block.get("pack_folder", "x"), str) + or "/" in str(block.get("pack_folder", "")) or not isinstance(folder, str) or not folder or "\\" in folder @@ -438,9 +440,37 @@ def _save_directory_blocks() -> list[str]: f"platforms/_registry.yml: {name}.save_directory.by_system and " f"platforms/{name}.yml lists no libretro_systems" ) + in_pack = block.get("pack_folder") or folder + taken = _bios_tree_below(name, in_pack) + if taken: + out.append( + f"platforms/_registry.yml: {name}.save_directory puts the save " + f"files in {in_pack}/, which the BIOS tree uses ({taken}): " + "name another pack_folder" + ) return out +def _bios_tree_below(platform: str, folder: str) -> str: + """A destination the platform declares below ``folder``, or empty.""" + from common import load_platform_config + + try: + config = load_platform_config(platform, str(ROOT / "platforms")) + except FileNotFoundError: + return "" + prefix = f"{folder}/".lower() + return next( + ( + str(entry["destination"]) + for system in (config.get("systems") or {}).values() + for entry in system.get("files") or [] + if str(entry.get("destination") or "").lower().startswith(prefix) + ), + "", + ) + + def _libretro_systems(platform: str) -> dict: """The platform file's core -> systems table, inheritance included.""" from common import load_platform_config diff --git a/tests/test_save_root.py b/tests/test_save_root.py index f130f894..f3c7eebc 100644 --- a/tests/test_save_root.py +++ b/tests/test_save_root.py @@ -134,7 +134,7 @@ class ThePlatformNamesItsSaveDirectory(unittest.TestCase): platforms = str(REPO_ROOT / "platforms") expected = { - "batocera": SaveLayout("saves", False, True), + "batocera": SaveLayout("saves", False, True, pack_folder="userdata-saves"), "knulli": SaveLayout("saves", False, True), "recalbox": SaveLayout("saves", False, True), "retrodeck": SaveLayout("saves", False, True), @@ -168,6 +168,25 @@ class ThePlatformNamesItsSaveDirectory(unittest.TestCase): "saves/wii/dolphin-emu/User/GBA/gba_bios.bin", ) + def test_a_pack_folder_apart_from_the_bios_tree(self): + """Batocera's BIOS tree has a saves/ of its own: the pack keeps the + save directory's files in another folder, the installer still + writes them to /userdata/saves.""" + layout = save_layout({"save_directory": { + "folder": "saves", "by_core": False, "by_system": True, + "pack_folder": "userdata-saves"}}) + self.assertEqual(layout.in_pack, "userdata-saves") + self.assertEqual(save_destination("stella/KVS1.WAV", None, layout, "atari2600"), + "userdata-saves/atari2600/stella/KVS1.WAV") + self.assertEqual(SaveLayout("saves", True).in_pack, "saves") + try: + from validate_schemas import _bios_tree_below + except ImportError: + self.skipTest("validate_schemas needs jsonschema") + self.assertEqual(_bios_tree_below("batocera", "saves"), + "saves/dolphin-emu/Triforce/segaboot.gcm") + self.assertEqual(_bios_tree_below("batocera", "userdata-saves"), "") + def test_the_systems_a_core_runs_for(self): from packpaths import libretro_systems diff --git a/wiki/adding-a-platform.md b/wiki/adding-a-platform.md index ee83fd5a..8b6411fc 100644 --- a/wiki/adding-a-platform.md +++ b/wiki/adding-a-platform.md @@ -304,6 +304,8 @@ that text belongs in a guide field. # (Batocera, RetroBat): one copy per system in the # platform file's libretro_systems by_core_for: [dolphin] # cores the launcher alone sorts by core (RetroBat) + pack_folder: userdata-saves # the pack's folder for them, where the BIOS tree + # already has one named like folder (Batocera) # null when saves sit beside each game zip_digest: composite # the frontend hashes a .zip by its members only firmware_folder_flat: true # the frontend lists each system folder without