mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-11 12:33:53 -06:00
feat: install files into retrobat emulator folders
RetroBat's Pico8 generator runs pico8.exe from AppConfig.GetFullPath: the emulatorLauncher.cfg key, else the first existing of four folders of the RetroBat install. The registry describes that search (emulators_directory) and the manifest writes such a file as root: emulators. The installer places it only when the BIOS folder sits beside RetroBat's launcher config and the config does not move the folder, and names it no_emulators_directory otherwise. Packs skip it as they skip root: roms. The PICO-8 0.2.7 Windows build (pico8.exe, SDL2.dll) is collected from the archive.org copy of pico-8_0.2.7_windows.zip, its pico8.dat being the 0.2.7 one already held.
This commit is contained in:
1 parent
f47c8b8e5a
commit
c8fe8d914d
16 files changed
+329
-79
No files matched your search
Binary file not shown.
Binary file not shown.
+1
-1
@@ -16,7 +16,7 @@
|
||||
# and Invoke-WebRequest throws on a cut transfer before the hash is read:
|
||||
# the site's build rewrites this line there (scripts/sitebootstrap.py).
|
||||
$mismatchAdvice = "The download was most likely cut short: try again. Within minutes of an update of this project the two files can come from caches of different ages. Waiting five minutes settles that."
|
||||
$defaultInstallSha256 = "3f44e2c6757db61c78a1df6781a0bbb78294a66cda5752617fc2d3b501847bf0"
|
||||
$defaultInstallSha256 = "b9586fece4058b670a4fe83b8c2825b7882024eba816a36353b07a7496af7f1b"
|
||||
$maximumInstallerBytes = 2MB
|
||||
$installerExit = 0
|
||||
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
|
||||
|
||||
+105
-3
@@ -143,6 +143,7 @@ OMISSION_REASONS = {
|
||||
"no_save_system": "no system runs the core",
|
||||
"outside_bios_folder": "read outside the BIOS folder",
|
||||
"no_roms_directory": "no roms folder",
|
||||
"no_emulators_directory": "no emulator folder",
|
||||
}
|
||||
# What a file is when it is not firmware, as the manifest's category field
|
||||
# says, and as the installer words it. Firmware carries no category.
|
||||
@@ -3594,6 +3595,9 @@ class SavePlace(NamedTuple):
|
||||
_SAVE_DIRS: dict[Path, SavePlace] = {}
|
||||
# BIOS folder -> the platform's roms folder, set by place_roms_entries.
|
||||
_ROMS_DIRS: dict[Path, Path] = {}
|
||||
# (BIOS folder, emulator key) -> the emulator folder, set by
|
||||
# place_emulator_entries.
|
||||
_EMULATOR_DIRS: dict[tuple[Path, str], Path] = {}
|
||||
|
||||
|
||||
def _entry_place(root: Path, entry: dict) -> tuple[Path, object]:
|
||||
@@ -3609,6 +3613,12 @@ def _entry_place(root: Path, entry: dict) -> tuple[Path, object]:
|
||||
if roms is None:
|
||||
raise ValueError(f"no roms folder known for {entry.get('dest')}")
|
||||
return roms, entry.get("dest")
|
||||
if entry.get("root") == "emulators":
|
||||
key, _, rest = str(entry.get("dest")).partition("/")
|
||||
folder = _EMULATOR_DIRS.get((root, key))
|
||||
if folder is None:
|
||||
raise ValueError(f"no emulator folder known for {entry.get('dest')}")
|
||||
return folder, rest
|
||||
if entry.get("root") != "save":
|
||||
return root, entry.get("dest")
|
||||
place = _SAVE_DIRS.get(root)
|
||||
@@ -3643,7 +3653,7 @@ def _entry_key(entry: dict) -> str:
|
||||
if root == "emulator":
|
||||
# A path in one emulator's own folder: two emulators read two files.
|
||||
return f"emulator\0{','.join(entry.get('cores') or [])}\0{dest}"
|
||||
return f"{root}\0{dest}" if root in ("save", "roms") else dest
|
||||
return f"{root}\0{dest}" if root in ("save", "roms", "emulators") else dest
|
||||
|
||||
|
||||
def _check_root(entry: dict, dest: str, omitted: bool = False) -> None:
|
||||
@@ -3651,8 +3661,10 @@ def _check_root(entry: dict, dest: str, omitted: bool = False) -> None:
|
||||
the emulator's own folder, outside the BIOS tree), and core_folder one
|
||||
folder name of a save entry."""
|
||||
root = entry.get("root")
|
||||
if root not in (None, "save", "roms") and not (omitted and root == "emulator"):
|
||||
if root not in (None, "save", "roms", "emulators") and not (omitted and root == "emulator"):
|
||||
raise ValueError(f"invalid root for {dest}")
|
||||
if root == "emulators" and len(PurePosixPath(dest).parts) < 2:
|
||||
raise ValueError(f"an emulators entry names its folder first: {dest}")
|
||||
folder = entry.get("core_folder")
|
||||
if folder is None:
|
||||
return
|
||||
@@ -3681,6 +3693,32 @@ def _check_roms_directory(data: dict) -> None:
|
||||
raise ValueError("invalid roms_directory")
|
||||
|
||||
|
||||
def _check_emulators_directory(data: dict) -> None:
|
||||
"""The manifest's emulators_directory, which every installable root: emulators entry needs."""
|
||||
layout = data.get("emulators_directory")
|
||||
installable = [
|
||||
entry for field in ("files", "case_variants")
|
||||
for entry in data.get(field) or [] if isinstance(entry, dict)
|
||||
]
|
||||
if layout is None:
|
||||
if any(entry.get("root") == "emulators" for entry in installable):
|
||||
raise ValueError("entries in emulator folders and no emulators_directory")
|
||||
return
|
||||
if not isinstance(layout, dict) or set(layout) != {"bios_folder", "searched", "default", "config"}:
|
||||
raise ValueError("invalid emulators_directory")
|
||||
searched = layout["searched"]
|
||||
if (
|
||||
not isinstance(searched, list) or not searched or layout["default"] not in searched
|
||||
or len(_safe_relative_path(layout["bios_folder"], "emulators_directory.bios_folder").parts) != 1
|
||||
):
|
||||
raise ValueError("invalid emulators_directory")
|
||||
for template in searched:
|
||||
if not isinstance(template, str) or template.count("{key}") != 1:
|
||||
raise ValueError("invalid emulators_directory.searched")
|
||||
_safe_relative_path(template.replace("{key}", "key"), "emulators_directory.searched")
|
||||
_safe_relative_path(layout["config"], "emulators_directory.config")
|
||||
|
||||
|
||||
def _check_save_directory(data: dict) -> None:
|
||||
"""The manifest's save_directory, which every root: save entry needs."""
|
||||
layout = data.get("save_directory")
|
||||
@@ -3985,6 +4023,7 @@ def _validate_manifest(data: object, plat: str) -> dict:
|
||||
_check_data_directory(entry, f"data_directories[{index}]")
|
||||
_check_save_directory(data)
|
||||
_check_roms_directory(data)
|
||||
_check_emulators_directory(data)
|
||||
|
||||
declared_total_files = data.get("total_files")
|
||||
if declared_total_files is not None and declared_total_files != len(files):
|
||||
@@ -5280,6 +5319,67 @@ def place_roms_entries(
|
||||
return kept, unplaced
|
||||
|
||||
|
||||
def _config_keys(path: Path) -> set[str]:
|
||||
"""The keys an emulatorLauncher-style key=value file sets, lowercased."""
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8", errors="replace")
|
||||
except OSError:
|
||||
return set()
|
||||
return {
|
||||
line.split("=", 1)[0].strip().lower()
|
||||
for line in text.splitlines() if "=" in line and line.split("=", 1)[1].strip()
|
||||
}
|
||||
|
||||
|
||||
def place_emulator_entries(
|
||||
files: list[dict], bios_path: Path, manifest: dict, display: str, say: bool = True,
|
||||
) -> tuple[list[dict], list[dict]]:
|
||||
"""Find the emulator folder each entry a launcher runs from one goes to.
|
||||
|
||||
The platform keeps its BIOS folder beside its own folder holding the
|
||||
launcher config (RetroBat's bios beside emulationstation): a BIOS
|
||||
folder of that name whose parent holds the config is the platform's.
|
||||
A key the config sets moves the folder somewhere this run does not
|
||||
follow, so its files are left out. Otherwise the folder is the first of
|
||||
the searched places that exists, the default one when none does, as the
|
||||
launcher searches them.
|
||||
"""
|
||||
inside = [entry for entry in files if entry.get("root") == "emulators"]
|
||||
if not inside:
|
||||
return files, []
|
||||
layout = manifest.get("emulators_directory") or {}
|
||||
home = bios_path.parent
|
||||
config = home / layout["config"] if layout else None
|
||||
matches = bool(layout) and bios_path.name == layout.get("bios_folder") and config.is_file()
|
||||
moved = _config_keys(config) if matches else set()
|
||||
kept: list[dict] = []
|
||||
unplaced: list[dict] = []
|
||||
for entry in files:
|
||||
if entry.get("root") != "emulators":
|
||||
kept.append(entry)
|
||||
continue
|
||||
key = str(entry["dest"]).split("/", 1)[0]
|
||||
if matches and key.lower() not in moved:
|
||||
places = [home / t.replace("{key}", key) for t in layout["searched"]]
|
||||
found = next((place for place in places if place.is_dir()), None)
|
||||
_EMULATOR_DIRS[(bios_path, key)] = found or home / layout["default"].replace("{key}", key)
|
||||
kept.append(entry)
|
||||
continue
|
||||
unplaced.append({
|
||||
"dest": entry["dest"], "name": PurePosixPath(entry["dest"]).name,
|
||||
"system": "", "systems": list(entry.get("systems") or []), "required": False,
|
||||
"reason": "no_emulators_directory", "cores": entry.get("cores"),
|
||||
"root": "emulators",
|
||||
})
|
||||
if unplaced and say:
|
||||
why = (f"{config} sets their folder" if matches
|
||||
else f"{bios_path} is not the {layout.get('bios_folder') or 'BIOS'} folder "
|
||||
f"beside {layout.get('config') or 'its config'}")
|
||||
print(f" {_plural(len(unplaced), 'file')} read from {display}'s emulator folders "
|
||||
f"left out: {why}.")
|
||||
return kept, unplaced
|
||||
|
||||
|
||||
# Per BIOS folder, where its save files go in words, or why that is not
|
||||
# known: read by the lines that count them once a choice is made.
|
||||
_SAVE_WHERE: dict[Path, str] = {}
|
||||
@@ -15894,7 +15994,9 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
|
||||
files, unplaced = place_save_entries(
|
||||
files, bios_path, manifest, args.save_dir, display, say=False)
|
||||
files, beyond = place_roms_entries(files, bios_path, manifest, display, say=not listing)
|
||||
unplaced = [*unplaced, *beyond]
|
||||
files, elsewhere = place_emulator_entries(
|
||||
files, bios_path, manifest, display, say=not listing)
|
||||
unplaced = [*unplaced, *beyond, *elsewhere]
|
||||
if not manifest.get("save_directory"):
|
||||
# Its own list leaves these files out: naming the folder with
|
||||
# --save-dir installs none of them, and was advised in a loop.
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/ins
|
||||
# curl or wget stop on a cut transfer before the hash is read: the site's
|
||||
# build rewrites this line there (scripts/sitebootstrap.py).
|
||||
MISMATCH_ADVICE="The download was most likely cut short: try again. Within minutes of an update of this project the two files can come from caches of different ages. Waiting five minutes settles that."
|
||||
DEFAULT_INSTALL_SHA256="3f44e2c6757db61c78a1df6781a0bbb78294a66cda5752617fc2d3b501847bf0"
|
||||
DEFAULT_INSTALL_SHA256="b9586fece4058b670a4fe83b8c2825b7882024eba816a36353b07a7496af7f1b"
|
||||
MAX_INSTALLER_BYTES=2097152
|
||||
|
||||
cleanup() {
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
{
|
||||
"dest": "roms/ports/retrobios/install.py",
|
||||
"repo_path": "install.py",
|
||||
"size": 1133449,
|
||||
"sha256": "3f44e2c6757db61c78a1df6781a0bbb78294a66cda5752617fc2d3b501847bf0",
|
||||
"size": 1138424,
|
||||
"sha256": "b9586fece4058b670a4fe83b8c2825b7882024eba816a36353b07a7496af7f1b",
|
||||
"executable": false
|
||||
},
|
||||
{
|
||||
@@ -71,8 +71,8 @@
|
||||
"muos": {
|
||||
"archive": {
|
||||
"dest": "ARCHIVE/RetroBIOS.muxapp",
|
||||
"size": 1187074,
|
||||
"sha256": "f98b3e8613e5572430097d4a0133d914986d17c47999020459a8555695a9a427"
|
||||
"size": 1192049,
|
||||
"sha256": "9da63b607a6ae2a734f4437c224b484229c514b08bbab6341b529ce102de464f"
|
||||
},
|
||||
"members": [
|
||||
{
|
||||
@@ -92,8 +92,8 @@
|
||||
{
|
||||
"name": "RetroBIOS/install.py",
|
||||
"repo_path": "install.py",
|
||||
"size": 1133449,
|
||||
"sha256": "3f44e2c6757db61c78a1df6781a0bbb78294a66cda5752617fc2d3b501847bf0",
|
||||
"size": 1138424,
|
||||
"sha256": "b9586fece4058b670a4fe83b8c2825b7882024eba816a36353b07a7496af7f1b",
|
||||
"executable": false
|
||||
},
|
||||
{
|
||||
@@ -166,8 +166,8 @@
|
||||
{
|
||||
"dest": ".config/modules/retrobios/install.py",
|
||||
"repo_path": "install.py",
|
||||
"size": 1133449,
|
||||
"sha256": "3f44e2c6757db61c78a1df6781a0bbb78294a66cda5752617fc2d3b501847bf0",
|
||||
"size": 1138424,
|
||||
"sha256": "b9586fece4058b670a4fe83b8c2825b7882024eba816a36353b07a7496af7f1b",
|
||||
"executable": false
|
||||
},
|
||||
{
|
||||
|
||||
@@ -590,6 +590,23 @@ platforms:
|
||||
hash_type: md5
|
||||
# Windows: names differing only by case land on one file.
|
||||
case_insensitive_fs: true
|
||||
# A generator takes an emulator folder from AppConfig.GetFullPath(key):
|
||||
# the key in emulatorLauncher.cfg, else the first existing of
|
||||
# emulationstation/<key>, emulators/<key>, system/emulators/<key> and
|
||||
# system/<key> below the RetroBat folder (EmulatorLauncher.Common/
|
||||
# FileFormats/ConfigFile.cs:126-180 at ca486005). emulatorLauncher runs
|
||||
# from emulationstation/, its LocalPath (emulatorLauncher/Program.cs:536-537),
|
||||
# beside bios/.
|
||||
emulators_directory:
|
||||
launcher_spelling: '<AppConfig.GetFullPath("{key}")>'
|
||||
bios_folder: bios
|
||||
searched:
|
||||
- "emulationstation/{key}"
|
||||
- "emulators/{key}"
|
||||
- "system/emulators/{key}"
|
||||
- "system/{key}"
|
||||
default: "emulators/{key}"
|
||||
config: emulationstation/emulatorLauncher.cfg
|
||||
# emulatorLauncher hands each libretro core saves\<system> at every
|
||||
# launch, saves\ beside bios\ (emulatorLauncher.cfg in retrobat 8.3.1),
|
||||
# and sorts saves by core for dolphin alone (CoreSaveSort)
|
||||
|
||||
@@ -179,7 +179,7 @@
|
||||
"dest": {
|
||||
"type": "string",
|
||||
"pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$)).+$",
|
||||
"description": "Where the file would go, below base_destination, below the save directory when root is save, below the roms folder when root is roms, or in the emulator's own folder when root is emulator."
|
||||
"description": "Where the file would go, below base_destination, below the save directory when root is save, below the roms folder when root is roms, in an emulator folder when root is emulators (its key first), or in the emulator's own folder when root is emulator."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
@@ -206,7 +206,8 @@
|
||||
"no_core_folder",
|
||||
"no_save_system",
|
||||
"outside_bios_folder",
|
||||
"no_roms_directory"
|
||||
"no_roms_directory",
|
||||
"no_emulators_directory"
|
||||
],
|
||||
"description": "Why the file is left out, as a code. enumDescriptions gives the meaning of each code, in the order of enum.",
|
||||
"enumDescriptions": [
|
||||
@@ -220,7 +221,8 @@
|
||||
"The frontend sorts saves by core and the core's profile does not name its folder (core_folder).",
|
||||
"The launcher gives each system its own save folder and no system of the platform runs the core.",
|
||||
"The platform runs the emulator from a folder outside the BIOS tree and links none of the BIOS tree where the emulator reads this file (root: emulator).",
|
||||
"The platform's launcher runs the file from its roms folder (root: roms) and the run cannot tell where that folder is: the BIOS folder is not the one the platform keeps in it."
|
||||
"The platform's launcher runs the file from its roms folder (root: roms) and the run cannot tell where that folder is: the BIOS folder is not the one the platform keeps in it.",
|
||||
"The platform's launcher runs the file from one of its emulator folders (root: emulators) and the run cannot place it there: the BIOS folder is not the one beside the platform's launcher config, or that config sets the folder."
|
||||
]
|
||||
},
|
||||
"category": {
|
||||
@@ -365,11 +367,12 @@
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"description": "save: dest is relative to the save directory the frontend hands the core (RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY), not to the BIOS directory. emulator: dest is the path the emulator reads in its own folder, which the platform keeps outside the BIOS tree (reason outside_bios_folder). roms: dest is relative to the platform's roms folder (roms_directory). Absent for a file of the BIOS tree.",
|
||||
"description": "save: dest is relative to the save directory the frontend hands the core (RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY), not to the BIOS directory. emulator: dest is the path the emulator reads in its own folder, which the platform keeps outside the BIOS tree (reason outside_bios_folder). roms: dest is relative to the platform's roms folder (roms_directory). emulators: dest is the emulator folder's key then the path in it (emulators_directory). Absent for a file of the BIOS tree.",
|
||||
"enum": [
|
||||
"save",
|
||||
"emulator",
|
||||
"roms"
|
||||
"roms",
|
||||
"emulators"
|
||||
]
|
||||
},
|
||||
"core_folder": {
|
||||
@@ -538,6 +541,40 @@
|
||||
"pattern": "^[^/\\\\]+$"
|
||||
}
|
||||
}
|
||||
},
|
||||
"emulators_directory": {
|
||||
"description": "Where the platform keeps its emulator folders, present when an entry carries root: emulators. Paths are relative to the folder holding the BIOS folder, which is named bios_folder. searched are the places the launcher tries for a key, in order, default the one the installer creates when none exists, and config the launcher's key=value file: a key it sets moves the folder, and its files are left out (no_emulators_directory).",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"bios_folder",
|
||||
"searched",
|
||||
"default",
|
||||
"config"
|
||||
],
|
||||
"properties": {
|
||||
"bios_folder": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "^[^/\\\\]+$"
|
||||
},
|
||||
"searched": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$)).*\\{key\\}.*$"
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"type": "string",
|
||||
"pattern": "\\{key\\}"
|
||||
},
|
||||
"config": {
|
||||
"type": "string",
|
||||
"pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$)).+$"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
@@ -556,7 +593,7 @@
|
||||
"dest": {
|
||||
"type": "string",
|
||||
"pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$)).+$",
|
||||
"description": "Path below base_destination, below the save directory when root is save, or below the roms folder when root is roms."
|
||||
"description": "Path below base_destination, below the save directory when root is save, below the roms folder when root is roms, or in an emulator folder when root is emulators (its key first)."
|
||||
},
|
||||
"sha1": {
|
||||
"type": "string",
|
||||
@@ -1039,10 +1076,11 @@
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"description": "save: dest is relative to the save directory the frontend hands the core (RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY), not to the BIOS directory. roms: dest is relative to the platform's roms folder, which roms_directory locates (a file its launcher runs from there, ROCKNIX's roms/pico-8). Absent for a file of the BIOS tree.",
|
||||
"description": "save: dest is relative to the save directory the frontend hands the core (RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY), not to the BIOS directory. roms: dest is relative to the platform's roms folder, which roms_directory locates (a file its launcher runs from there, ROCKNIX's roms/pico-8). emulators: dest is the key of an emulator folder of the platform, then the path in it, located by emulators_directory (RetroBat's emulators/pico8). Absent for a file of the BIOS tree.",
|
||||
"enum": [
|
||||
"save",
|
||||
"roms"
|
||||
"roms",
|
||||
"emulators"
|
||||
]
|
||||
},
|
||||
"core_folder": {
|
||||
|
||||
+8
-3
@@ -2775,8 +2775,12 @@ def _executed(standalone_cores: Iterable[str]) -> list:
|
||||
# Why a standalone entry has no destination on a platform that relocates its
|
||||
# layout: the manifest's omission reason, named as the installer words it.
|
||||
OUTSIDE_BIOS_FOLDER = "outside_bios_folder"
|
||||
# The entry root of a file read below the platform's roms directory.
|
||||
# The entry root of a file read below the platform's roms directory, and of
|
||||
# one read in an emulator's folder outside the BIOS tree that the registry
|
||||
# describes. Each is located by the registry entry <root>_directory.
|
||||
ROMS_ROOT = "roms"
|
||||
EMULATORS_ROOT = "emulators"
|
||||
OUTER_ROOTS = (ROMS_ROOT, EMULATORS_ROOT)
|
||||
|
||||
|
||||
def standalone_layout_path(entry: dict, profile: dict) -> str | None:
|
||||
@@ -2870,8 +2874,9 @@ def platform_layouts(
|
||||
moved = {**f, "standalone_path": path,
|
||||
"layout_path": standalone_layout_path(f, profile)}
|
||||
# A file the launcher runs from the roms directory (ROCKNIX's
|
||||
# roms/pico-8) is relative to it, and says so.
|
||||
files.append({**moved, "root": ROMS_ROOT} if tree else moved)
|
||||
# roms/pico-8) or an emulator folder (RetroBat's emulators/pico8)
|
||||
# is relative to it, and says so.
|
||||
files.append({**moved, "root": tree} if tree else moved)
|
||||
standalone = {**profile, "files": files}
|
||||
dual = {c.removeprefix(DUAL_MODE_PREFIX) for c in standalone_cores
|
||||
if c.startswith(DUAL_MODE_PREFIX)}
|
||||
|
||||
@@ -24,6 +24,7 @@ from pathlib import Path
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from common import (
|
||||
OUTER_ROOTS,
|
||||
platform_arg,
|
||||
ProfileSelectionError,
|
||||
_foreign_tree,
|
||||
@@ -867,7 +868,7 @@ def _cross_reference_profile(
|
||||
# archive.
|
||||
# A platform declares files of its BIOS tree: none answers for one
|
||||
# the core reads below the save directory.
|
||||
in_platform = not save_root and f.get("root") != "roms" and declaration_settles(
|
||||
in_platform = not save_root and f.get("root") not in OUTER_ROOTS and declaration_settles(
|
||||
f, fname, platform_names, effective_path, index.get("folded_slots", frozenset()),
|
||||
index.get("declared_contents"),
|
||||
)
|
||||
@@ -955,8 +956,8 @@ def _cross_reference_profile(
|
||||
entry["root"] = "save"
|
||||
elif f.get("unplaced"):
|
||||
entry["unplaced"] = f["unplaced"]
|
||||
elif f.get("root") == "roms":
|
||||
entry["root"] = "roms"
|
||||
elif f.get("root") in OUTER_ROOTS:
|
||||
entry["root"] = f["root"]
|
||||
elif index.get("launcher_candidates") is not None:
|
||||
# What verify hands the launcher layer for the same entry.
|
||||
probe = {
|
||||
|
||||
+30
-19
@@ -38,7 +38,7 @@ from pathlib import Path, PurePosixPath
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from common import resolve_platform_cores as _platform_cores
|
||||
from common import (
|
||||
ROMS_ROOT,
|
||||
OUTER_ROOTS,
|
||||
installed_data_refs,
|
||||
platform_arg,
|
||||
require_directory,
|
||||
@@ -1303,9 +1303,10 @@ def generate_pack(
|
||||
continue
|
||||
if not dest:
|
||||
continue
|
||||
if fe.get("root") == ROMS_ROOT:
|
||||
# Read below the roms directory: the pack is extracted into
|
||||
# the BIOS folder and cannot reach it, the installer places it.
|
||||
if fe.get("root") in OUTER_ROOTS:
|
||||
# Read below the roms directory or in an emulator folder: the
|
||||
# pack is extracted into the BIOS folder and cannot reach
|
||||
# them, the installer places these files.
|
||||
continue
|
||||
# Core extras: _collect_emulator_extras already adjusted
|
||||
# destinations for slug-based platforms. Apply the effective
|
||||
@@ -4453,11 +4454,11 @@ def _manifest_core_entries(
|
||||
# extras prefix when it is an internal layout dir (RetroDECK bios/).
|
||||
# A file read below the save directory is written relative to the
|
||||
# directory the core is handed, and says so (save_entry_fields).
|
||||
roms_root = fe.get("root") == ROMS_ROOT
|
||||
outer_root = fe.get("root") if fe.get("root") in OUTER_ROOTS else None
|
||||
if save_root:
|
||||
manifest_dest = save_manifest_dest(fe)
|
||||
elif roms_root:
|
||||
# Relative to the roms directory, which the entry names.
|
||||
elif outer_root:
|
||||
# Relative to the roms or emulators folder the entry names.
|
||||
manifest_dest = dest
|
||||
else:
|
||||
manifest_dest = _manifest_dest_of(full_dest, base_dest)
|
||||
@@ -4469,8 +4470,8 @@ def _manifest_core_entries(
|
||||
)
|
||||
if entry is not None and save_root:
|
||||
entry.update(save_entry_fields(fe))
|
||||
elif entry is not None and roms_root:
|
||||
entry["root"] = ROMS_ROOT
|
||||
elif entry is not None and outer_root:
|
||||
entry["root"] = outer_root
|
||||
if entry is None:
|
||||
if not _claim_platform_omission(
|
||||
omitted_by_destination.get(full_dest), source_emu
|
||||
@@ -4630,6 +4631,13 @@ def _record_absent(
|
||||
)
|
||||
|
||||
|
||||
# What a manifest's roms_directory and emulators_directory carry from the
|
||||
# registry: the BIOS folder's name, and for emulator folders the places the
|
||||
# launcher searches, the one a missing folder is created at, and the config
|
||||
# file whose key moves a folder.
|
||||
MANIFEST_OUTER_FIELDS = ("bios_folder", "searched", "default", "config")
|
||||
|
||||
|
||||
def _record_unplaceable(unplaceable: list[dict], record_omission: Callable[..., None]) -> None:
|
||||
"""Held by the collection, nowhere to put it on this platform.
|
||||
|
||||
@@ -4639,8 +4647,8 @@ def _record_unplaceable(unplaceable: list[dict], record_omission: Callable[...,
|
||||
where = u.get("path") or u.get("name", "")
|
||||
if u.get("unplaced"):
|
||||
key = f"emulator:{u.get('profile') or u.get('emulator', '')}:{where}"
|
||||
elif u.get("root") == ROMS_ROOT:
|
||||
key = f"roms:{where}"
|
||||
elif u.get("root") in OUTER_ROOTS:
|
||||
key = f"{u['root']}:{where}"
|
||||
elif u.get("root") == "save":
|
||||
key = f"save:{save_manifest_dest(u)}"
|
||||
else:
|
||||
@@ -4832,10 +4840,10 @@ def generate_manifest(
|
||||
if outside:
|
||||
dest_fields: dict = {"dest": sanitize_pack_path(file_entry.get("path") or ""),
|
||||
"root": "emulator"}
|
||||
elif file_entry.get("root") == ROMS_ROOT:
|
||||
elif file_entry.get("root") in OUTER_ROOTS:
|
||||
dest_fields = {"dest": sanitize_pack_path(
|
||||
file_entry.get("destination") or file_entry.get("path") or ""
|
||||
), "root": ROMS_ROOT}
|
||||
), "root": file_entry["root"]}
|
||||
elif save_root:
|
||||
dest_fields = {"dest": save_manifest_dest(file_entry), **save_entry_fields(file_entry)}
|
||||
else:
|
||||
@@ -5195,12 +5203,15 @@ def generate_manifest(
|
||||
}
|
||||
if data_directories:
|
||||
result["data_directories"] = data_directories
|
||||
roms = config.get("roms_directory") or {}
|
||||
if any(entry.get("root") == ROMS_ROOT
|
||||
for entry in [*manifest_files, *result["omitted_files"]]) and roms:
|
||||
# Where the installer finds the roms directory: the folder holding
|
||||
# the BIOS folder, which the platform names bios_folder.
|
||||
result["roms_directory"] = {"bios_folder": str(roms.get("bios_folder") or "")}
|
||||
for root in OUTER_ROOTS:
|
||||
layout = config.get(f"{root}_directory") or {}
|
||||
if layout and any(entry.get("root") == root
|
||||
for entry in [*manifest_files, *result["omitted_files"]]):
|
||||
# Where the installer finds the folder: relative to the folder
|
||||
# holding the BIOS folder, which the platform names bios_folder.
|
||||
result[f"{root}_directory"] = {
|
||||
key: layout[key] for key in MANIFEST_OUTER_FIELDS if key in layout
|
||||
}
|
||||
saved = [entry for entry in manifest_files if entry.get("root") == "save"]
|
||||
if saved:
|
||||
layout = save_layout(config)
|
||||
|
||||
@@ -329,6 +329,8 @@ EXECUTABLE_DIRECTORY = "executable directory"
|
||||
# relative to, and the roms directory of a platform whose registry names it.
|
||||
BIOS_TREE = ""
|
||||
ROMS_TREE = "roms"
|
||||
# The emulator folders of a platform whose registry describes them.
|
||||
EMULATORS_TREE = "emulators"
|
||||
# A folder of neither tree, which no pack and no installer can fill.
|
||||
OUTSIDE_TREE = "outside"
|
||||
Place = tuple[str, str]
|
||||
@@ -448,6 +450,22 @@ def folder_in_roms(directory: str, config: dict) -> str | None:
|
||||
return None if re.search(r"[\s<>$:;]", rest) else rest
|
||||
|
||||
|
||||
def folder_in_emulators(directory: str, config: dict) -> str | None:
|
||||
"""The emulator folder a launcher names by its key, None for any other.
|
||||
|
||||
Only a platform whose registry describes its emulator folders has one
|
||||
(``emulators_directory.launcher_spelling``): RetroBat's generators take
|
||||
each from AppConfig.GetFullPath("<key>"), a folder of the RetroBat
|
||||
install named after the key.
|
||||
"""
|
||||
spelling = str((config.get("emulators_directory") or {}).get("launcher_spelling") or "")
|
||||
if "{key}" not in spelling:
|
||||
return None
|
||||
pattern = re.escape(spelling).replace(re.escape("{key}"), r"([\w.-]+)")
|
||||
found = re.fullmatch(pattern, directory.strip())
|
||||
return found.group(1) if found else None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ExecutedFile:
|
||||
"""A file a platform's launcher executes, and where it lies.
|
||||
@@ -474,6 +492,7 @@ def executed_files(config: dict) -> list[ExecutedFile]:
|
||||
raw = str(record.get("search_dir") or "").strip()
|
||||
directories = _directories(record, base)
|
||||
roms = folder_in_roms(raw, config) if raw and not directories else None
|
||||
emulator_folder = folder_in_emulators(raw, config) if raw and not directories else None
|
||||
# A program file needs no extension (pico8, vox): the names are
|
||||
# taken as written, placeholders left out.
|
||||
for name in (str(n).strip() for n in record.get("names") or []):
|
||||
@@ -485,6 +504,8 @@ def executed_files(config: dict) -> list[ExecutedFile]:
|
||||
))
|
||||
elif roms is not None:
|
||||
place = (ROMS_TREE, sanitize_pack_path(f"{roms}/{name}" if roms else name))
|
||||
elif emulator_folder is not None:
|
||||
place = (EMULATORS_TREE, sanitize_pack_path(f"{emulator_folder}/{name}"))
|
||||
else:
|
||||
place = None
|
||||
for emulator in (e.strip() for e in str(record.get("emulator", "")).split(",")):
|
||||
|
||||
+28
-23
@@ -17,7 +17,7 @@ from common import resolve_local_file
|
||||
from common import resolve_platform_cores
|
||||
from common import core_system_dirs, platform_layouts, system_dir_path
|
||||
from common import standalone_set as platform_standalone_set
|
||||
from common import ROMS_ROOT, sanitize_pack_path
|
||||
from common import OUTER_ROOTS, sanitize_pack_path
|
||||
from common import size_fits
|
||||
from validation import deliverable, read_from_save_dir, read_from_system_dir
|
||||
from packpaths import libretro_systems, save_destination, save_layout
|
||||
@@ -43,22 +43,23 @@ def extra_full_destination(fe: dict, prefix: str) -> str:
|
||||
"""
|
||||
if fe.get("root") == "save":
|
||||
return str(fe.get("pack_path") or "")
|
||||
if fe.get("root") == ROMS_ROOT:
|
||||
return roms_destination(sanitize_pack_path(fe.get("destination", fe.get("name", ""))))
|
||||
if fe.get("root") in OUTER_ROOTS:
|
||||
return outer_destination(
|
||||
fe["root"], sanitize_pack_path(fe.get("destination", fe.get("name", "")))
|
||||
)
|
||||
return prefixed_destination(
|
||||
sanitize_pack_path(fe.get("destination", fe.get("name", ""))), prefix
|
||||
)
|
||||
|
||||
|
||||
# Keys a destination below the roms directory apart from the BIOS tree's
|
||||
# paths. Never written into a pack: the pack is extracted into the BIOS
|
||||
# folder, and the installer places these files (``root: roms``).
|
||||
ROMS_KEY = "roms:"
|
||||
def outer_destination(root: str, dest: str) -> str:
|
||||
"""The key of a destination below the roms or an emulator folder.
|
||||
|
||||
|
||||
def roms_destination(dest: str) -> str:
|
||||
"""The key of a destination below the roms directory."""
|
||||
return f"{ROMS_KEY}{dest}"
|
||||
Kept apart from the BIOS tree's paths and never written into a pack:
|
||||
the pack is extracted into the BIOS folder, and the installer places
|
||||
these files (``root: roms``, ``root: emulators``).
|
||||
"""
|
||||
return f"{root}:{dest}"
|
||||
|
||||
|
||||
def prefixed_destination(dest: str, prefix: str) -> str:
|
||||
@@ -479,7 +480,7 @@ def unplaceable_extras(
|
||||
for u in undeclared:
|
||||
if not u.get("in_repo") and not u.get("unplaced"):
|
||||
continue
|
||||
if (not placement.slug_based and u.get("root") not in ("save", ROMS_ROOT)
|
||||
if (not placement.slug_based and u.get("root") not in ("save", *OUTER_ROOTS)
|
||||
and not u.get("unplaced")):
|
||||
continue
|
||||
dest = _extra_dest(u)
|
||||
@@ -567,7 +568,7 @@ def _agnostic_scan_extras(
|
||||
# The scan walks the BIOS tree: a file read below the save
|
||||
# directory is placed by the gap report's pass.
|
||||
if (read_from_save_dir(f, standalone) or f.get("unplaced")
|
||||
or f.get("root") == ROMS_ROOT):
|
||||
or f.get("root") in OUTER_ROOTS):
|
||||
continue
|
||||
# An agnostic BIOS mode says the BIOS filename is free, not that
|
||||
# every file the emulator loads is. A free filename still has a
|
||||
@@ -1018,9 +1019,12 @@ class ExtraPlacement:
|
||||
self.native_systems = native_slug_systems(config)
|
||||
self.flat = bool(config.get("firmware_folder_flat"))
|
||||
self.save = save_layout(config)
|
||||
# The roms directory the registry names, for a file a launcher runs
|
||||
# from it (ROCKNIX's roms/pico-8).
|
||||
self.roms = bool((config.get("roms_directory") or {}).get("path"))
|
||||
# The roms directory and emulator folders the registry describes,
|
||||
# for a file a launcher runs from them (ROCKNIX's roms/pico-8,
|
||||
# RetroBat's emulators/pico8).
|
||||
self.outer = {
|
||||
root for root in OUTER_ROOTS if config.get(f"{root}_directory")
|
||||
}
|
||||
self.config = config
|
||||
self.norm_map = (
|
||||
{_norm_system_id(sid): sid for sid in self.platform_systems}
|
||||
@@ -1041,8 +1045,9 @@ class ExtraPlacement:
|
||||
"""
|
||||
if entry.get("unplaced"):
|
||||
return None
|
||||
if entry.get("root") == ROMS_ROOT:
|
||||
return (dest, roms_destination(dest)) if self.roms else None
|
||||
if entry.get("root") in OUTER_ROOTS:
|
||||
root = entry["root"]
|
||||
return (dest, outer_destination(root, dest)) if root in self.outer else None
|
||||
if entry.get("root") == "save":
|
||||
placed = self.save_places(entry, dest)
|
||||
return placed[0] if placed else None
|
||||
@@ -1103,8 +1108,8 @@ class ExtraPlacement:
|
||||
"""Why ``place`` returned None for this entry."""
|
||||
if entry.get("unplaced"):
|
||||
return str(entry["unplaced"])
|
||||
if entry.get("root") == ROMS_ROOT:
|
||||
return "no_roms_directory"
|
||||
if entry.get("root") in OUTER_ROOTS:
|
||||
return f"no_{entry['root']}_directory"
|
||||
if entry.get("root") == "save":
|
||||
if self.save is None:
|
||||
return "no_save_directory"
|
||||
@@ -1329,7 +1334,7 @@ def _collect_emulator_extras(
|
||||
# covered elsewhere belongs to another program's file.
|
||||
# Nor is one placed outside the BIOS tree by the layout.
|
||||
if (fname not in covered_names or f.get("unsourceable") or f.get("unplaced")
|
||||
or f.get("root") == ROMS_ROOT):
|
||||
or f.get("root") in OUTER_ROOTS):
|
||||
continue
|
||||
# Installed with the emulator or by the platform's package,
|
||||
# as the first pass already decided for the same entry.
|
||||
@@ -1568,8 +1573,8 @@ def _report_extra(u: dict, name: str, dest: str) -> dict:
|
||||
extra["root"] = "save"
|
||||
if u.get("core_folder") is not None:
|
||||
extra["core_folder"] = u["core_folder"]
|
||||
elif u.get("root") == ROMS_ROOT:
|
||||
extra["root"] = ROMS_ROOT
|
||||
elif u.get("root") in OUTER_ROOTS:
|
||||
extra["root"] = u["root"]
|
||||
if u.get("system_dir"):
|
||||
extra["system_dir"] = u["system_dir"]
|
||||
if u.get("layout_path"):
|
||||
|
||||
+13
-9
@@ -38,6 +38,7 @@ import slots
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from hashing import parse_md5_list
|
||||
from common import (
|
||||
OUTER_ROOTS,
|
||||
_foreign_tree,
|
||||
_owner_tokens,
|
||||
platform_arg,
|
||||
@@ -596,7 +597,7 @@ def find_undeclared_files(
|
||||
# of its own: two cores reading hiscore.dat there read
|
||||
# two files.
|
||||
("save", core_save_folder(profile)) if save_root
|
||||
else ("roms",) if f.get("root") == "roms"
|
||||
else (f["root"],) if f.get("root") in OUTER_ROOTS
|
||||
# In the emulator's own folder: no other core reads it.
|
||||
else ("emulator", emu_name) if f.get("unplaced") else None,
|
||||
)
|
||||
@@ -609,9 +610,9 @@ def find_undeclared_files(
|
||||
# Nor does it answer for one read below the roms directory.
|
||||
verdict = _candidate_verdict(
|
||||
f, fname, is_standalone, include_all,
|
||||
{} if save_root or f.get("root") == "roms" else declared_names,
|
||||
{} if save_root or f.get("root") in OUTER_ROOTS else declared_names,
|
||||
dest, folded_slots,
|
||||
None if save_root or f.get("root") == "roms" else declared_contents,
|
||||
None if save_root or f.get("root") in OUTER_ROOTS else declared_contents,
|
||||
)
|
||||
if verdict == "settled":
|
||||
seen_files.add(seen_key)
|
||||
@@ -772,9 +773,10 @@ def find_undeclared_files(
|
||||
entry["layout_path"] = f["layout_path"]
|
||||
if f.get("unplaced"):
|
||||
entry["unplaced"] = f["unplaced"]
|
||||
elif f.get("root") == "roms":
|
||||
# Read below the roms directory (ROCKNIX's roms/pico-8).
|
||||
entry["root"] = "roms"
|
||||
elif f.get("root") in OUTER_ROOTS:
|
||||
# Read below the roms directory (ROCKNIX's roms/pico-8)
|
||||
# or in an emulator folder (RetroBat's emulators/pico8).
|
||||
entry["root"] = f["root"]
|
||||
elif launcher_layer and not save_root:
|
||||
launcher_candidates.append((emu_name, is_standalone, f, entry))
|
||||
if duplicate:
|
||||
@@ -885,6 +887,7 @@ def find_exclusion_notes(
|
||||
_UNPLACED_REASONS = {
|
||||
"outside_bios_folder": "read in a folder the platform keeps outside the BIOS tree",
|
||||
"no_roms_directory": "read below a roms folder the platform's registry does not name",
|
||||
"no_emulators_directory": "read in an emulator folder the platform's registry does not describe",
|
||||
"no_platform_slug": "no platform slug for its system",
|
||||
"below_platform_folder": "below the folder the platform lists",
|
||||
"no_save_directory": "read below a save directory the platform keeps in no fixed place",
|
||||
@@ -908,11 +911,12 @@ def _mark_unplaced(config: dict, undeclared: list[dict], profiles: dict) -> None
|
||||
reason = unplaced.get(
|
||||
(u.get("emulator", ""), u.get("name", ""), u.get("path") or "")
|
||||
)
|
||||
if not reason and u.get("root") == "roms":
|
||||
if not reason and u.get("root") in OUTER_ROOTS:
|
||||
# The pack is extracted into the BIOS folder: the installer
|
||||
# writes this file into the roms folder, the pack cannot.
|
||||
# writes this file into the roms or emulator folder, the pack
|
||||
# cannot.
|
||||
u["in_pack"] = False
|
||||
u["omitted"] = "read below the roms folder, which only the installer reaches"
|
||||
u["omitted"] = f"read below the {u['root']} folder, which only the installer reaches"
|
||||
continue
|
||||
if reason:
|
||||
u["in_pack"] = False
|
||||
|
||||
@@ -262,6 +262,7 @@ class AProgramRunFromItsFolder(unittest.TestCase):
|
||||
"biosplat": [{**run, "names": ["prog/prog"], "search_dir": "/userdata/bios"}],
|
||||
"romsplat": [{**run, "names": ["prog"], "search_dir": "/storage/roms/prog"}],
|
||||
"exeplat": [{**run, "names": ["prog"], "search_dir": "<emulator folder>"}],
|
||||
"emuplat": [{**run, "names": ["prog"], "search_dir": '<GetPath("prog")>'}],
|
||||
"plainplat": [],
|
||||
}
|
||||
for name, records in launchers.items():
|
||||
@@ -275,6 +276,10 @@ class AProgramRunFromItsFolder(unittest.TestCase):
|
||||
**{name: {"config": f"{name}.yml"} for name in launchers},
|
||||
"romsplat": {"config": "romsplat.yml",
|
||||
"roms_directory": {"path": "/storage/roms", "bios_folder": "bios"}},
|
||||
"emuplat": {"config": "emuplat.yml", "emulators_directory": {
|
||||
"launcher_spelling": '<GetPath("{key}")>', "bios_folder": "bios",
|
||||
"searched": ["frontend/{key}", "emulators/{key}"],
|
||||
"default": "emulators/{key}", "config": "frontend/launcher.cfg"}},
|
||||
}}))
|
||||
(self.emulators / "prog.yml").write_text(yaml.safe_dump({
|
||||
"emulator": "Prog", "type": "standalone", "cores": ["prog"],
|
||||
@@ -363,6 +368,43 @@ class AProgramRunFromItsFolder(unittest.TestCase):
|
||||
with self.assertRaises(ValueError):
|
||||
install._check_roms_directory({"files": files})
|
||||
|
||||
def test_run_from_an_emulator_folder_the_registry_describes(self) -> None:
|
||||
self.assertEqual(self.pack("emuplat"), [])
|
||||
manifest = self.manifest("emuplat")
|
||||
self.assertEqual(
|
||||
sorted((f["dest"], f.get("root")) for f in manifest["files"]),
|
||||
[("prog/prog", "emulators"), ("prog/prog.dat", "emulators")],
|
||||
)
|
||||
self.assertEqual(manifest["emulators_directory"]["default"], "emulators/{key}")
|
||||
self.assertTrue(all(u["in_pack"] is False for u in self.verify("emuplat")))
|
||||
|
||||
def test_the_installer_follows_the_launcher_search(self) -> None:
|
||||
import install
|
||||
|
||||
manifest = self.manifest("emuplat")
|
||||
install._check_emulators_directory(manifest)
|
||||
files = list(manifest["files"])
|
||||
home = self.root / "Frontend"
|
||||
bios = home / "bios"
|
||||
bios.mkdir(parents=True)
|
||||
# Not the platform's folder: no launcher config beside it.
|
||||
kept, left = install.place_emulator_entries(files, bios, manifest, "emuplat", say=False)
|
||||
self.assertEqual((kept, {o["reason"] for o in left}), ([], {"no_emulators_directory"}))
|
||||
(home / "frontend").mkdir()
|
||||
(home / "frontend" / "launcher.cfg").write_text("other=x\n")
|
||||
kept, left = install.place_emulator_entries(files, bios, manifest, "emuplat", say=False)
|
||||
self.assertEqual((len(kept), left), (2, []))
|
||||
entry = next(f for f in kept if f["dest"] == "prog/prog")
|
||||
self.assertEqual(install._entry_place(bios, entry), (home / "emulators" / "prog", "prog"))
|
||||
# The first searched place that exists wins.
|
||||
(home / "frontend" / "prog").mkdir()
|
||||
install.place_emulator_entries(files, bios, manifest, "emuplat", say=False)
|
||||
self.assertEqual(install._entry_place(bios, entry)[0], home / "frontend" / "prog")
|
||||
# A key the launcher config sets moves the folder: left out.
|
||||
(home / "frontend" / "launcher.cfg").write_text("prog=D:\\Prog\n")
|
||||
kept, left = install.place_emulator_entries(files, bios, manifest, "emuplat", say=False)
|
||||
self.assertEqual((kept, len(left)), ([], 2))
|
||||
|
||||
|
||||
class EachProgramItsOwnProfile(unittest.TestCase):
|
||||
"""PICO-8 and Voxatron are two programs: a platform running one gets its files only."""
|
||||
|
||||
@@ -222,7 +222,11 @@ directory: RetroPie starts UAE4All2 in `$md_inst`, whose `kickstarts` it links t
|
||||
`BIOS/amiga`. A profile declaring `executable directory` is bound where a record
|
||||
executes one of its files: Batocera runs `pico-8/pico8` from `bios`, ROCKNIX
|
||||
`pico8_64` from `/storage/roms/pico-8`, below the `roms_directory` its registry entry
|
||||
names, which the manifest writes as `root: roms`. Two records giving two folders
|
||||
names, which the manifest writes as `root: roms`. RetroBat runs `pico8.exe` from an
|
||||
emulator folder its generators name by key: the registry's `emulators_directory`
|
||||
spells that key and lists the places the launcher searches, and the manifest writes
|
||||
`root: emulators`, which the installer places only beside RetroBat's own launcher
|
||||
config. Two records giving two folders
|
||||
leave the profile's layout. Once a platform binds part of a layout, an entry no
|
||||
binding covers is read in a folder outside the BIOS tree: it is never packed, and
|
||||
the manifest names it (`root: emulator`, `outside_bios_folder`).
|
||||
|
||||
Reference in new issue
Block a user