chore: merge voxatron split and emulator folders

This commit is contained in:
Abdessamad Derraz committed 2026-11-09 18:56:51 +01:00
commit 8633db0a22
19 files changed
+478 -171

No files matched your search

Binary file not shown.
Binary file not shown.
+9 -90
View File
@@ -12,30 +12,30 @@ version_basis:
version_source:
url: "https://www.lexaloffle.com/dl/docs/pico-8_changelog.txt"
pattern: '^\s*v(0\.\d+\.\d+[a-z]?)\s*$'
display_name: "Lexaloffle - PICO-8 / Voxatron"
display_name: "Lexaloffle - PICO-8"
cores:
- lexaloffle
- "pico-8"
- vox_official
- "pico8"
- "Pico-8"
- "pico_standalone"
- "float-scaled"
systems:
- pico8
- voxatron
standalone_directories:
- "executable directory"
notes: |
Commercial fantasy consoles by Lexaloffle Games LLP, sold as ZIP archives
per platform. Nothing in either program checks a hash of its own files:
the hashes below are those of the files in the official 0.2.7 archives.
Commercial fantasy console by Lexaloffle Games LLP, sold as ZIP archives
per platform. Nothing in the program checks a hash of its own files: the
hashes below are those of the files in the official 0.2.7 archives.
Voxatron, the same publisher's other program, is profiled apart
(voxatron.yml).
Each program finds its own folder through /proc/self/exe and reads its
The program finds its own folder through /proc/self/exe and reads its
.dat beside itself. The paths below are the layout of the archives, whose
top folders are pico-8/ and voxatron/: a launcher that runs one of these
files from a folder places the program folder there.
top folder is pico-8/: a launcher that runs one of these files from a
folder places the program folder there.
PICO-8 0.2.7 ships five archives. pico-8_0.2.7_amd64.zip holds pico8
(x86_64, SDL2 linked in) and pico8_dyn (x86_64, system libSDL2).
@@ -83,11 +83,6 @@ notes: |
and three other home folders on linuxarm, pico8 on linux and unix, and
pico8.exe on Windows.
Voxatron: Batocera's generator runs bios/voxatron/vox with
LD_LIBRARY_PATH set to bios/voxatron. x86_64 only. vox_dyn is the
variant linked against the system SDL2. libHoloPlayCore.so is the Looking
Glass SDK the binary links against.
files:
# -------------------------------------------------------
# PICO-8 0.2.7
@@ -234,79 +229,3 @@ files:
description: "SPLORE favourites list"
source_ref: "pico-8 0.2.7 amd64 pico8, string favourites.txt at 0x242346"
note: "Read from the -home folder. The bbs/<listing>.pip.png images beside it are downloaded from the BBS."
# -------------------------------------------------------
# Voxatron
# -------------------------------------------------------
- name: vox
path: voxatron/vox
system: voxatron
required: true
description: "Voxatron main executable"
source_ref: "batocera lexaloffleGenerator.py:20, 42, 48-52"
note: "The Voxatron program vox, an x86_64 ELF with SDL2 linked in. Batocera's generator runs bios/voxatron/vox and raises when it is missing or not executable."
- name: vox_dyn
path: voxatron/vox_dyn
system: voxatron
required: false
description: "Voxatron dynamically linked executable"
source_ref: "voxatron vox_dyn, NEEDED libSDL2-2.0.so.0, batocera lexaloffleGenerator.py:20, 42"
note: "The same program linked against the system SDL2 (its only extra NEEDED library). Batocera's generator never runs it."
- name: vox.dat
path: voxatron/vox.dat
system: voxatron
required: true
description: "Voxatron data file"
source_ref: "voxatron vox, string vox.dat at 0x2a58f, voxatron vox_dyn, string vox.dat at 0x1f1af"
note: "Opened from the folder of the binary that runs. Absent, vox stops with \"FATAL ERROR: could not load vox.dat\"."
- name: libHoloPlayCore.so
path: voxatron/libHoloPlayCore.so
system: voxatron
required: false
description: "Looking Glass holographic display support library"
source_ref: "voxatron vox, voxatron vox_dyn, batocera lexaloffleGenerator.py:41, 87-90"
note: "Neither binary links it or names it, and neither opens it in a run (strace, with and without the file). Batocera's generator sets LD_LIBRARY_PATH to bios/voxatron, where it keeps the library."
- name: config.txt
unsourceable: "written by the emulator on exit, no canonical file"
system: voxatron
required: false
description: "Voxatron settings"
source_ref: "voxatron vox, string config.txt at 0x27bea0"
note: "Read beside the binary, then from ~/.lexaloffle/Voxatron, and written there when absent."
- name: sdl_controllers.txt
unsourceable: "user supplied, no canonical file"
system: voxatron
required: false
description: "Custom SDL controller mappings"
source_ref: "voxatron vox, string sdl_controllers.txt at 0x2847f7"
note: "Read beside the binary, then from ~/.lexaloffle/Voxatron, where Voxatron writes it when absent."
- name: favourites.txt
unsourceable: "written by the emulator, no canonical file"
system: voxatron
required: false
description: "Cart browser favourites list"
source_ref: "voxatron vox, string favourites.txt at 0x27a39e"
note: "Read from ~/.lexaloffle/Voxatron."
- name: internal_carts.txt
unsourceable: "written by the emulator, no canonical file"
system: voxatron
required: false
description: "Voxatron internal cart list"
source_ref: "voxatron vox, string internal_carts.txt at 0x27a446"
note: "Read from ~/.lexaloffle/Voxatron and written there at startup."
- name: player_1.vpp
path: player/player_1.vpp
unsourceable: "written by the emulator, no canonical file"
system: voxatron
required: false
description: "Voxatron player profile"
source_ref: "voxatron vox, string player/player_%d.vpp at 0x273f90"
note: "Read from ~/.lexaloffle/Voxatron/player, and written there when absent."
+104
View File
@@ -0,0 +1,104 @@
emulator: Voxatron
type: standalone
core_classification: other
upstream: "https://www.lexaloffle.com/voxatron.php"
profiled_date: "2026-11-09"
core_version: "0.3.5b"
version_basis:
kind: unverified
why: no_artifact
display_name: "Lexaloffle - Voxatron"
cores:
- vox_official
systems:
- voxatron
standalone_directories:
- "executable directory"
notes: |
Commercial voxel fantasy console by Lexaloffle Games LLP, sold as ZIP
archives. The held binaries report voxatron 0.3.5b. Nothing in the
program checks a hash of its own files.
The program finds its own folder through /proc/self/exe and reads vox.dat
beside itself. The paths below put the program in voxatron/: a launcher
that runs vox from a folder places that folder there.
Batocera's generator runs bios/voxatron/vox with LD_LIBRARY_PATH set to
bios/voxatron. x86_64 only. vox_dyn is the variant linked against the
system SDL2. libHoloPlayCore.so is the Looking Glass SDK the binary links
against.
files:
- name: vox
path: voxatron/vox
system: voxatron
required: true
description: "Voxatron main executable"
source_ref: "batocera lexaloffleGenerator.py:20, 42, 48-52"
note: "The Voxatron program vox, an x86_64 ELF with SDL2 linked in. Batocera's generator runs bios/voxatron/vox and raises when it is missing or not executable."
- name: vox_dyn
path: voxatron/vox_dyn
system: voxatron
required: false
description: "Voxatron dynamically linked executable"
source_ref: "voxatron vox_dyn, NEEDED libSDL2-2.0.so.0, batocera lexaloffleGenerator.py:20, 42"
note: "The same program linked against the system SDL2 (its only extra NEEDED library). Batocera's generator never runs it."
- name: vox.dat
path: voxatron/vox.dat
system: voxatron
required: true
description: "Voxatron data file"
source_ref: "voxatron vox, string vox.dat at 0x2a58f, voxatron vox_dyn, string vox.dat at 0x1f1af"
note: "Opened from the folder of the binary that runs. Absent, vox stops with \"FATAL ERROR: could not load vox.dat\"."
- name: libHoloPlayCore.so
path: voxatron/libHoloPlayCore.so
system: voxatron
required: false
description: "Looking Glass holographic display support library"
source_ref: "voxatron vox, voxatron vox_dyn, batocera lexaloffleGenerator.py:41, 87-90"
note: "Neither binary links it or names it, and neither opens it in a run (strace, with and without the file). Batocera's generator sets LD_LIBRARY_PATH to bios/voxatron, where it keeps the library."
- name: config.txt
unsourceable: "written by the emulator on exit, no canonical file"
system: voxatron
required: false
description: "Voxatron settings"
source_ref: "voxatron vox, string config.txt at 0x27bea0"
note: "Read beside the binary, then from ~/.lexaloffle/Voxatron, and written there when absent."
- name: sdl_controllers.txt
unsourceable: "user supplied, no canonical file"
system: voxatron
required: false
description: "Custom SDL controller mappings"
source_ref: "voxatron vox, string sdl_controllers.txt at 0x2847f7"
note: "Read beside the binary, then from ~/.lexaloffle/Voxatron, where Voxatron writes it when absent."
- name: favourites.txt
unsourceable: "written by the emulator, no canonical file"
system: voxatron
required: false
description: "Cart browser favourites list"
source_ref: "voxatron vox, string favourites.txt at 0x27a39e"
note: "Read from ~/.lexaloffle/Voxatron."
- name: internal_carts.txt
unsourceable: "written by the emulator, no canonical file"
system: voxatron
required: false
description: "Voxatron internal cart list"
source_ref: "voxatron vox, string internal_carts.txt at 0x27a446"
note: "Read from ~/.lexaloffle/Voxatron and written there at startup."
- name: player_1.vpp
path: player/player_1.vpp
unsourceable: "written by the emulator, no canonical file"
system: voxatron
required: false
description: "Voxatron player profile"
source_ref: "voxatron vox, string player/player_%d.vpp at 0x273f90"
note: "Read from ~/.lexaloffle/Voxatron/player, and written there when absent."
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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() {
+8 -8
View File
@@ -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
},
{
+17
View File
@@ -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)
+46 -8
View File
@@ -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": {
+14 -4
View File
@@ -2810,8 +2810,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:
@@ -2905,8 +2909,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)}
@@ -3032,7 +3037,12 @@ def resolve_platform_cores(
if cores_config == "all_libretro":
result = every_libretro()
elif isinstance(cores_config, list):
core_set = {str(c) for c in cores_config}
# An emulator the platform launches standalone runs there even when
# its own list names only the cores: Batocera's es_systems.yml files
# lexaloffle and gsplus as standalone emulators and nowhere else.
core_set = {str(c) for c in cores_config} | {
str(c) for c in config.get("standalone_cores") or []
}
index = upstream_profile_index(profiles)
platform = str(config.get("_key") or "")
+4 -3
View File
@@ -25,6 +25,7 @@ from pathlib import Path
sys.path.insert(0, os.path.dirname(__file__))
from alternatives import set_keys, set_requirements, settle_sets
from common import (
OUTER_ROOTS,
platform_arg,
ProfileSelectionError,
_foreign_tree,
@@ -875,7 +876,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"),
)
@@ -973,8 +974,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
View File
@@ -39,7 +39,7 @@ sys.path.insert(0, os.path.dirname(__file__))
from alternatives import core_file_spares, settle_sets, slot_set_keys
from common import resolve_platform_cores as _platform_cores
from common import (
ROMS_ROOT,
OUTER_ROOTS,
installed_data_refs,
platform_arg,
require_directory,
@@ -1309,9 +1309,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
@@ -4508,11 +4509,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)
@@ -4524,8 +4525,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
@@ -4685,6 +4686,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.
@@ -4694,8 +4702,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:
@@ -4887,10 +4895,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:
@@ -5250,12 +5258,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)
+21
View File
@@ -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
View File
@@ -18,7 +18,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
@@ -44,22 +44,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:
@@ -484,7 +485,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)
@@ -572,7 +573,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
@@ -1023,9 +1024,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}
@@ -1046,8 +1050,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
@@ -1108,8 +1113,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"
@@ -1334,7 +1339,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.
@@ -1575,8 +1580,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
View File
@@ -46,6 +46,7 @@ from alternatives import (
slot_set_keys,
)
from common import (
OUTER_ROOTS,
_foreign_tree,
_owner_tokens,
platform_arg,
@@ -607,7 +608,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,
)
@@ -620,9 +621,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)
@@ -795,9 +796,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))
keys = set_keys((emu_name, is_standalone), f)
@@ -975,6 +977,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",
@@ -998,11 +1001,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
+2 -1
View File
@@ -57,7 +57,8 @@ NAMED_FOR_A_LAUNCHER = {
# Profiles whose entries still rest on a launcher, with the fact that blocks
# reading the emulator itself.
LAUNCHER_SOURCED = {
"lexaloffle": "PICO-8 and Voxatron are sold binaries, and no copy of either is held to read",
"lexaloffle": "PICO-8 is a sold binary, and no copy is held to read",
"voxatron": "Voxatron is a sold binary, and no copy is held to read",
}
_SENTENCE = re.compile(r"(?<=[.!?])\s+(?=[A-Z0-9(\"'])")
+70
View File
@@ -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,71 @@ 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."""
def test_only_the_platforms_that_run_voxatron_get_it(self) -> None:
from common import list_registered_platforms, resolve_platform_cores
profiles = load_emulator_profiles(str(REPO_ROOT / "emulators"))
runs = {
name: {k for k in ("lexaloffle", "voxatron")
if k in resolve_platform_cores(
load_platform_config(name, str(REPO_ROOT / "platforms")), profiles)}
for name in list_registered_platforms(str(REPO_ROOT / "platforms"))
}
self.assertEqual(
{name for name, found in runs.items() if "voxatron" in found}, {"batocera", "knulli"}
)
self.assertTrue(all("lexaloffle" in runs[name] for name in (
"batocera", "knulli", "rocknix", "retrodeck", "retrobat", "recalbox")))
def test_a_standalone_emulator_is_run(self) -> None:
from common import resolve_platform_cores
profiles = {"emu": {"emulator": "Emu", "type": "standalone", "cores": ["emu"],
"systems": ["sys"], "files": []}}
config = {"cores": ["other"], "standalone_cores": ["emu"], "systems": {}}
self.assertEqual(resolve_platform_cores(config, profiles), {"emu"})
if __name__ == "__main__":
unittest.main()
+5 -1
View File
@@ -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`).