mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-12 04:53:52 -06:00
chore: merge launcher placement
This commit is contained in:
commit
56ed37c3f0
32 files changed
+1289
-160
No files matched your search
+117
-31
@@ -1482,6 +1482,29 @@ def resolve_local_file(
|
||||
system_dir = file_entry.get("system_dir")
|
||||
if system_dir and dest_hint.startswith(f"{system_dir}/"):
|
||||
dest_hint = dest_hint[len(system_dir) + 1:]
|
||||
# A launcher that moves the layout (EmuDeck links azahar's sysdata to
|
||||
# bios/azahar/keys) changes where the file goes, not the tail the
|
||||
# collection indexes it under. Both are asked, and the stronger proof
|
||||
# wins: a content or path match over a name, the destination on a tie.
|
||||
layout = file_entry.get("layout_path")
|
||||
if layout and layout != dest_hint:
|
||||
plain = {k: v for k, v in file_entry.items() if k != "layout_path"}
|
||||
answers = [
|
||||
resolve_local_file(
|
||||
plain, db, zip_contents, dest_hint=hint, _depth=_depth,
|
||||
data_dir_registry=data_dir_registry,
|
||||
)
|
||||
for hint in (dest_hint, str(layout))
|
||||
]
|
||||
for found in answers:
|
||||
if found[0] is not None and (
|
||||
resolution_is_hash_exact(found[1]) or found[1] == "path_exact"
|
||||
):
|
||||
return found
|
||||
for found in answers:
|
||||
if found[0] is not None and found[1] not in ("not_found", "hash_mismatch"):
|
||||
return found
|
||||
return next((found for found in answers if found[0] is not None), answers[0])
|
||||
# A name written with its directory is the destination when no path is
|
||||
# given: gam4980/E.BIN resolved by its basename took another model's OS
|
||||
# in verify --emulator and the emulator pack, while the platform pack,
|
||||
@@ -2624,6 +2647,9 @@ HANDED_PREFIX = "handed:"
|
||||
# Marks, in the same set, a libretro core and the system directory the
|
||||
# platform hands it (``core_system_dirs``), as JSON.
|
||||
CORE_DIR_PREFIX = "coredir:"
|
||||
# Marks, in the same set, a file the platform's launcher executes
|
||||
# (launchers.executed_files), as JSON.
|
||||
EXECUTED_PREFIX = "exec:"
|
||||
|
||||
|
||||
def core_system_dirs(
|
||||
@@ -2689,14 +2715,19 @@ def standalone_set(config: dict) -> set[str]:
|
||||
for core in entry.get("cores") or []
|
||||
}
|
||||
if names - {n for n in names if n.startswith(CORE_DIR_PREFIX)}:
|
||||
from launchers import handed_directories # noqa: PLC0415
|
||||
from launchers import executed_files, handed_directories # noqa: PLC0415
|
||||
|
||||
names |= {
|
||||
HANDED_PREFIX + json.dumps(
|
||||
[h.emulator, h.folder, h.directory, sorted(h.handles), h.source_ref]
|
||||
[h.emulator, h.folder, h.directory, sorted(h.handles), h.source_ref,
|
||||
h.roms_folder]
|
||||
)
|
||||
for h in handed_directories(config)
|
||||
}
|
||||
names |= {
|
||||
EXECUTED_PREFIX + json.dumps([run.emulator, run.name, run.place, run.source_ref])
|
||||
for run in executed_files(config)
|
||||
}
|
||||
return names
|
||||
|
||||
|
||||
@@ -2724,38 +2755,81 @@ def _handed(standalone_cores: Iterable[str]) -> list:
|
||||
for tag in standalone_cores:
|
||||
if not tag.startswith(HANDED_PREFIX):
|
||||
continue
|
||||
emulator, folder, directory, handles, ref = json.loads(tag[len(HANDED_PREFIX):])
|
||||
found.append(HandedDirectory(emulator, folder, directory, frozenset(handles), ref))
|
||||
emulator, folder, directory, handles, ref, roms = json.loads(tag[len(HANDED_PREFIX):])
|
||||
found.append(HandedDirectory(emulator, folder, directory, frozenset(handles), ref, roms))
|
||||
return found
|
||||
|
||||
|
||||
def standalone_destination(entry: dict, root: str | None) -> str:
|
||||
"""Where a standalone build reads an entry, relative to the BIOS root.
|
||||
def _executed(standalone_cores: Iterable[str]) -> list:
|
||||
"""The files a standalone set's launchers execute, as launchers read them."""
|
||||
from launchers import ExecutedFile # noqa: PLC0415
|
||||
|
||||
``standalone_path`` is relative to the directories the build resolves
|
||||
it against (``standalone_directories``). A platform that hands one of
|
||||
them a folder of its BIOS tree puts the file under that folder:
|
||||
RetroPie and ROCKNIX give Flycast bios/dc, EmuDeck bios/flycast/bios.
|
||||
An entry with no ``standalone_path`` keeps ``path``, the layout both
|
||||
builds share.
|
||||
found = []
|
||||
for tag in standalone_cores:
|
||||
if tag.startswith(EXECUTED_PREFIX):
|
||||
emulator, name, place, ref = json.loads(tag[len(EXECUTED_PREFIX):])
|
||||
found.append(ExecutedFile(emulator, name, tuple(place) if place else None, ref))
|
||||
return found
|
||||
|
||||
|
||||
# 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.
|
||||
ROMS_ROOT = "roms"
|
||||
|
||||
|
||||
def standalone_layout_path(entry: dict, profile: dict) -> str | None:
|
||||
"""The path of an entry relative to the directories its standalone build declares.
|
||||
|
||||
``standalone_path`` when the entry has one. A profile with no libretro
|
||||
build has one layout, so its ``path`` is that layout: UAE4All2 reads
|
||||
kickstarts/kick13.rom below its working directory.
|
||||
"""
|
||||
own = entry.get("standalone_path")
|
||||
if own and root:
|
||||
return f"{root}/{own}"
|
||||
return own or entry.get("path") or entry.get("name", "")
|
||||
if own:
|
||||
return own
|
||||
if "libretro" in str(profile.get("type", "")):
|
||||
return None
|
||||
return entry.get("path") or None
|
||||
|
||||
|
||||
def standalone_root_for(
|
||||
def standalone_destination(
|
||||
entry: dict, profile: dict, bindings: dict[str, tuple[str, str]]
|
||||
) -> tuple[str, str] | None:
|
||||
"""Where a platform puts a standalone entry: (tree, path).
|
||||
|
||||
``bindings`` are the places the platform's launchers give the layout
|
||||
(launchers.standalone_bindings): RetroPie and ROCKNIX give Flycast bios/dc,
|
||||
EmuDeck bios/flycast/bios, RetroPie gives UAE4All2's kickstarts/ bios/amiga,
|
||||
ROCKNIX runs PICO-8 from roms/pico-8. The tree is launchers.BIOS_TREE or
|
||||
launchers.ROMS_TREE. None when no binding covers the entry.
|
||||
"""
|
||||
layout = standalone_layout_path(entry, profile)
|
||||
if layout is None or not bindings:
|
||||
return None
|
||||
from launchers import bound_path # noqa: PLC0415
|
||||
|
||||
return bound_path(layout, bindings)
|
||||
|
||||
|
||||
def standalone_bindings_for(
|
||||
emu_name: str, profile: dict, standalone_cores: Iterable[str]
|
||||
) -> str | None:
|
||||
"""The folder a platform gives this emulator's standalone layout, or None."""
|
||||
) -> dict[str, tuple[str, str]]:
|
||||
"""The places a platform gives this emulator's standalone layout, by subfolder."""
|
||||
declared = profile.get("standalone_directories") or []
|
||||
if not declared:
|
||||
return None
|
||||
from launchers import standalone_root # noqa: PLC0415
|
||||
return {}
|
||||
from launchers import standalone_bindings # noqa: PLC0415
|
||||
|
||||
names = {emu_name, *(str(c) for c in profile.get("cores") or [])}
|
||||
return standalone_root(names, declared, _handed(standalone_cores))
|
||||
layout = [
|
||||
path for f in profile.get("files") or []
|
||||
if isinstance(f, dict) and (path := standalone_layout_path(f, profile))
|
||||
]
|
||||
return standalone_bindings(
|
||||
names, declared, _handed(standalone_cores), _executed(standalone_cores), layout
|
||||
)
|
||||
|
||||
|
||||
def platform_layouts(
|
||||
@@ -2774,19 +2848,31 @@ def platform_layouts(
|
||||
return [(profile, False)]
|
||||
return [(system_dir_view(profile, d), False) for d in directories]
|
||||
standalone = profile
|
||||
root = standalone_root_for(emu_name, profile, standalone_cores)
|
||||
if root:
|
||||
bindings = standalone_bindings_for(emu_name, profile, standalone_cores)
|
||||
if bindings:
|
||||
# The standalone view carries the platform's destination, so every
|
||||
# reader of standalone_path (builder, manifest, verify, slots) places
|
||||
# the file in the folder the platform hands the emulator.
|
||||
standalone = {
|
||||
**profile,
|
||||
"files": [
|
||||
{**f, "standalone_path": standalone_destination(f, root)}
|
||||
if isinstance(f, dict) and f.get("standalone_path") else f
|
||||
for f in profile.get("files") or []
|
||||
],
|
||||
}
|
||||
# An entry of the layout no binding covers is read in a folder the
|
||||
# platform keeps outside the BIOS tree: UAE4All2's kick.rom in
|
||||
# RetroPie's $md_inst. It is marked, never left at the BIOS root
|
||||
# where the emulator does not read it.
|
||||
files = []
|
||||
for f in profile.get("files") or []:
|
||||
if not isinstance(f, dict) or standalone_layout_path(f, profile) is None:
|
||||
files.append(f)
|
||||
continue
|
||||
placed = standalone_destination(f, profile, bindings)
|
||||
if placed is None or placed[0] == "outside":
|
||||
files.append({**f, "unplaced": OUTSIDE_BIOS_FOLDER})
|
||||
continue
|
||||
tree, path = placed
|
||||
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)
|
||||
standalone = {**profile, "files": files}
|
||||
dual = {c.removeprefix(DUAL_MODE_PREFIX) for c in standalone_cores
|
||||
if c.startswith(DUAL_MODE_PREFIX)}
|
||||
if "libretro" in str(profile.get("type", "")) and dual & {
|
||||
|
||||
@@ -856,7 +856,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 declaration_settles(
|
||||
in_platform = not save_root and f.get("root") != "roms" and declaration_settles(
|
||||
f, fname, platform_names, effective_path, index.get("folded_slots", frozenset())
|
||||
)
|
||||
|
||||
@@ -941,6 +941,10 @@ def _cross_reference_profile(
|
||||
}
|
||||
if save_root:
|
||||
entry["root"] = "save"
|
||||
elif f.get("unplaced"):
|
||||
entry["unplaced"] = f["unplaced"]
|
||||
elif f.get("root") == "roms":
|
||||
entry["root"] = "roms"
|
||||
elif index.get("launcher_candidates") is not None:
|
||||
# What verify hands the launcher layer for the same entry.
|
||||
probe = {
|
||||
|
||||
+48
-11
@@ -38,6 +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,
|
||||
installed_data_refs,
|
||||
platform_arg,
|
||||
require_directory,
|
||||
@@ -1302,6 +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.
|
||||
continue
|
||||
# Core extras: _collect_emulator_extras already adjusted
|
||||
# destinations for slug-based platforms. Apply the effective
|
||||
# prefix (base_dest, or inferred from YAML when base_dest is
|
||||
@@ -4256,7 +4261,8 @@ def _attach_claims(
|
||||
})
|
||||
continue
|
||||
claim_path, _ = validated_choice(
|
||||
claim, claim_path, db, validation_index or {}, bios_dir, None, dest, owners,
|
||||
claim, claim_path, db, validation_index or {}, bios_dir, None,
|
||||
claim.get("layout_path") or dest, owners,
|
||||
)
|
||||
copy, _size = _manifest_entry(
|
||||
claim_path, manifest_dest, [claim_emu], db, repo_root, {}
|
||||
@@ -4447,9 +4453,14 @@ 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).
|
||||
manifest_dest = (
|
||||
save_manifest_dest(fe) if save_root else _manifest_dest_of(full_dest, base_dest)
|
||||
)
|
||||
roms_root = fe.get("root") == ROMS_ROOT
|
||||
if save_root:
|
||||
manifest_dest = save_manifest_dest(fe)
|
||||
elif roms_root:
|
||||
# Relative to the roms directory, which the entry names.
|
||||
manifest_dest = dest
|
||||
else:
|
||||
manifest_dest = _manifest_dest_of(full_dest, base_dest)
|
||||
|
||||
entry, file_size = _manifest_entry(
|
||||
local_path, manifest_dest, [source_emu] if source_emu else [],
|
||||
@@ -4458,6 +4469,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
|
||||
if entry is None:
|
||||
if not _claim_platform_omission(
|
||||
omitted_by_destination.get(full_dest), source_emu
|
||||
@@ -4624,10 +4637,18 @@ def _record_unplaceable(unplaceable: list[dict], record_omission: Callable[...,
|
||||
"""
|
||||
for u in unplaceable:
|
||||
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") == "save":
|
||||
key = f"save:{save_manifest_dest(u)}"
|
||||
else:
|
||||
key = where
|
||||
record_omission(
|
||||
# Keyed apart from the BIOS tree's paths, which a file read below
|
||||
# the save directory can repeat.
|
||||
f"save:{save_manifest_dest(u)}" if u.get("root") == "save" else where,
|
||||
# the save directory or in the emulator's folder can repeat.
|
||||
key,
|
||||
u, u.get("system") or "",
|
||||
# The profile key, the name a target's core list and the
|
||||
# installer's narrowing match: the display name matched none.
|
||||
@@ -4805,12 +4826,22 @@ def generate_manifest(
|
||||
if cores is None else extra_region_key(file_entry, declared_root(config))
|
||||
)
|
||||
save_root = file_entry.get("root") == "save"
|
||||
# Read in the emulator's own folder, outside the BIOS tree: dest is
|
||||
# the path the emulator reads, relative to that folder.
|
||||
outside = bool(file_entry.get("unplaced"))
|
||||
if outside:
|
||||
dest_fields: dict = {"dest": sanitize_pack_path(file_entry.get("path") or ""),
|
||||
"root": "emulator"}
|
||||
elif file_entry.get("root") == ROMS_ROOT:
|
||||
dest_fields = {"dest": sanitize_pack_path(
|
||||
file_entry.get("destination") or file_entry.get("path") or ""
|
||||
), "root": ROMS_ROOT}
|
||||
elif save_root:
|
||||
dest_fields = {"dest": save_manifest_dest(file_entry), **save_entry_fields(file_entry)}
|
||||
else:
|
||||
dest_fields = {"dest": manifest_destination(full_destination)}
|
||||
omitted_by_destination[full_destination] = {
|
||||
"dest": (
|
||||
save_manifest_dest(file_entry) if save_root
|
||||
else manifest_destination(full_destination)
|
||||
),
|
||||
**(save_entry_fields(file_entry) if save_root else {}),
|
||||
**dest_fields,
|
||||
"name": str(file_entry.get("name") or ""),
|
||||
"system": system_id,
|
||||
"required": bool(file_entry.get("required", True)),
|
||||
@@ -5164,6 +5195,12 @@ 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 "")}
|
||||
saved = [entry for entry in manifest_files if entry.get("root") == "save"]
|
||||
if saved:
|
||||
layout = save_layout(config)
|
||||
|
||||
+192
-31
@@ -211,6 +211,24 @@ BIOS_ROOT_VARIABLES = ("$biosdir", "$home/RetroPie/BIOS", "${biosPath}", "$bios_
|
||||
# The kinds that hand an emulator a whole directory.
|
||||
HANDED_KINDS = ("links", "passes_path")
|
||||
|
||||
# How a launcher record says the platform starts the emulator in a directory
|
||||
# (retropie_launchers._working_directory), and the handle a profile gives
|
||||
# that directory in standalone_directories: the call its code reads it with.
|
||||
WORKING_DIRECTORY_PASSED = "working directory of "
|
||||
WORKING_DIRECTORY = "getcwd"
|
||||
# The handle a profile declares when its program reads its files beside its
|
||||
# own executable: a launcher record executing one of the profile's files
|
||||
# places the folder holding that file where the launcher runs it from.
|
||||
EXECUTABLE_DIRECTORY = "executable directory"
|
||||
|
||||
# The trees a placed folder belongs to: the BIOS tree every destination is
|
||||
# relative to, and the roms directory of a platform whose registry names it.
|
||||
BIOS_TREE = ""
|
||||
ROMS_TREE = "roms"
|
||||
# A folder of neither tree, which no pack and no installer can fill.
|
||||
OUTSIDE_TREE = "outside"
|
||||
Place = tuple[str, str]
|
||||
|
||||
_HOME = re.compile(r"^(?:\$\{HOME\}|\$HOME|\$home|~)(?=/|$)")
|
||||
# Flatpak sets XDG_DATA_HOME to ~/.var/app/<id>/data inside the sandbox.
|
||||
_FLATPAK_DATA = re.compile(r"^~/\.var/app/[^/]+/data(?=/|$)")
|
||||
@@ -258,6 +276,15 @@ class HandedDirectory:
|
||||
directory: str
|
||||
handles: frozenset[str]
|
||||
source_ref: str
|
||||
roms_folder: str | None = None
|
||||
|
||||
def place(self) -> Place | None:
|
||||
"""The folder as a place, None when it lies outside both roots."""
|
||||
if self.folder is not None:
|
||||
return (BIOS_TREE, self.folder)
|
||||
if self.roms_folder is not None:
|
||||
return (ROMS_TREE, self.roms_folder)
|
||||
return None
|
||||
|
||||
|
||||
def handed_directories(config: dict) -> list[HandedDirectory]:
|
||||
@@ -278,56 +305,190 @@ def handed_directories(config: dict) -> list[HandedDirectory]:
|
||||
if not raw or re.search(r"[\s,:;]", raw):
|
||||
continue
|
||||
directory = normalize_handle(raw)
|
||||
handles = frozenset(
|
||||
handle
|
||||
for token in str(record.get("passes_to") or "").split()
|
||||
if (handle := normalize_handle(token)) and handle != directory
|
||||
)
|
||||
passes_to = str(record.get("passes_to") or "")
|
||||
if passes_to.startswith(WORKING_DIRECTORY_PASSED):
|
||||
# The program runs from the directory: what its code reads
|
||||
# through getcwd(), or relative to it, is below it.
|
||||
handles = frozenset({WORKING_DIRECTORY})
|
||||
else:
|
||||
handles = frozenset(
|
||||
handle
|
||||
for token in passes_to.split()
|
||||
if (handle := normalize_handle(token)) and handle != directory
|
||||
)
|
||||
folder = _folder_in_bios(raw, base)
|
||||
for emulator in (e.strip() for e in str(record.get("emulator", "")).split(",")):
|
||||
found.append(HandedDirectory(
|
||||
emulator=emulator,
|
||||
folder=_folder_in_bios(raw, base),
|
||||
folder=folder,
|
||||
directory=directory,
|
||||
handles=handles,
|
||||
source_ref=str(record.get("source_ref", "")),
|
||||
roms_folder=None if folder is not None else folder_in_roms(raw, config),
|
||||
))
|
||||
return found
|
||||
|
||||
|
||||
def standalone_root(
|
||||
names: Iterable[str], declared: Iterable[str], handed: Iterable[HandedDirectory]
|
||||
) -> str | None:
|
||||
"""The folder of the BIOS tree a platform gives a standalone build's layout.
|
||||
def folder_in_roms(directory: str, config: dict) -> str | None:
|
||||
"""A directory relative to the platform's roms directory, None outside it.
|
||||
|
||||
``declared`` are the directories the build resolves ``standalone_path``
|
||||
against, as its profile names them (``standalone_directories``). A
|
||||
record handing a folder of the BIOS tree under one of them gives it
|
||||
directly: RetroPie points Flycast's ~/.local/share/flycast at bios/dc. A
|
||||
record handing a directory outside the tree under one of them is
|
||||
followed through a link to the tree: ROCKNIX's patched Flycast uses
|
||||
/storage/roms/dreamcast/data, which its start script links to bios/dc.
|
||||
Only a platform whose registry names the roms directory has one
|
||||
(``roms_directory.path``): ROCKNIX's /storage/roms, which holds its BIOS
|
||||
folder and the folders its launchers run programs from.
|
||||
"""
|
||||
roms = str((config.get("roms_directory") or {}).get("path") or "").rstrip("/")
|
||||
directory = directory.strip().rstrip("/")
|
||||
if not roms or not directory.startswith(roms + "/"):
|
||||
return None
|
||||
rest = directory[len(roms) + 1:]
|
||||
return None if re.search(r"[\s<>$:;]", rest) else rest
|
||||
|
||||
None when nothing gives a folder, or when two records give two: the
|
||||
layout is then the profile's, relative to the BIOS root.
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ExecutedFile:
|
||||
"""A file a platform's launcher executes, and where it lies.
|
||||
|
||||
``place`` is (root, path): the BIOS tree or the roms directory, None
|
||||
when the file lies outside both (RetroBat's emulator folders).
|
||||
"""
|
||||
|
||||
emulator: str
|
||||
name: str
|
||||
place: Place | None
|
||||
source_ref: str
|
||||
|
||||
|
||||
def executed_files(config: dict) -> list[ExecutedFile]:
|
||||
"""Every file a platform's launchers execute, from records passing it to exec."""
|
||||
base = str(config.get("base_destination") or "bios")
|
||||
found: list[ExecutedFile] = []
|
||||
for record in config.get("launchers") or []:
|
||||
if record.get("kind") not in ("fixed_name", "names_searched"):
|
||||
continue
|
||||
if str(record.get("passes_to") or "").strip() != "exec":
|
||||
continue
|
||||
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
|
||||
# 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 []):
|
||||
if not name or re.search(r"[<>{}]", name):
|
||||
continue
|
||||
if directories:
|
||||
place: Place | None = (BIOS_TREE, sanitize_pack_path(
|
||||
f"{directories[0]}/{name}" if directories[0] else name
|
||||
))
|
||||
elif roms is not None:
|
||||
place = (ROMS_TREE, sanitize_pack_path(f"{roms}/{name}" if roms else name))
|
||||
else:
|
||||
place = None
|
||||
for emulator in (e.strip() for e in str(record.get("emulator", "")).split(",")):
|
||||
found.append(ExecutedFile(
|
||||
emulator, name.rsplit("/", 1)[-1], place, str(record.get("source_ref", ""))
|
||||
))
|
||||
return found
|
||||
|
||||
|
||||
def standalone_bindings(
|
||||
names: Iterable[str],
|
||||
declared: Iterable[str],
|
||||
handed: Iterable[HandedDirectory],
|
||||
executed: Iterable[ExecutedFile] = (),
|
||||
layout: Iterable[str] = (),
|
||||
) -> dict[str, Place]:
|
||||
"""The folders a platform gives a standalone build's layout.
|
||||
|
||||
``declared`` are the directories the build resolves its paths against,
|
||||
as its profile names them (``standalone_directories``). The result maps
|
||||
a subfolder of that layout ("" for the whole of it) to the place, a
|
||||
folder of the BIOS tree or of the roms directory, the emulator reads it
|
||||
from.
|
||||
|
||||
A record handing a folder under a declared directory gives it directly:
|
||||
RetroPie points Flycast's ~/.local/share/flycast at bios/dc. A record
|
||||
handing a directory outside the trees is followed through the links that
|
||||
put a folder at it or below it: ROCKNIX's patched Flycast uses
|
||||
/storage/roms/dreamcast/data, which its start script links to bios/dc,
|
||||
and RetroPie starts UAE4All2 in $md_inst, whose kickstarts folder it
|
||||
links to bios/amiga.
|
||||
|
||||
A profile whose program reads its files beside its executable declares
|
||||
EXECUTABLE_DIRECTORY, and ``layout`` holds its paths: a launcher that
|
||||
executes one of them gives the folder holding it. Batocera runs
|
||||
pico-8/pico8 from its BIOS folder, ROCKNIX pico8_64 from
|
||||
/storage/roms/pico-8.
|
||||
|
||||
A subfolder two records give two places is left out.
|
||||
"""
|
||||
names = set(names)
|
||||
wanted = {normalize_handle(d) for d in declared}
|
||||
if not wanted:
|
||||
return None
|
||||
return {}
|
||||
records = [h for h in handed if h.emulator in names]
|
||||
folders: set[str] = set()
|
||||
|
||||
def below(handle: str, directory: str) -> str | None:
|
||||
if handle == directory:
|
||||
return ""
|
||||
if handle.startswith(directory + "/"):
|
||||
return handle[len(directory) + 1:]
|
||||
return None
|
||||
|
||||
found: dict[str, set[Place]] = {}
|
||||
for record in records:
|
||||
if not record.handles & wanted:
|
||||
subs = {
|
||||
sub for handle in record.handles for directory in wanted
|
||||
if (sub := below(handle, directory)) is not None
|
||||
}
|
||||
for sub in subs:
|
||||
if record.folder is not None:
|
||||
found.setdefault(sub, set()).add((BIOS_TREE, record.folder))
|
||||
continue
|
||||
# A link into the BIOS tree comes first: the pack fills that.
|
||||
linked = False
|
||||
for link in records:
|
||||
target = link.place()
|
||||
if target is None:
|
||||
continue
|
||||
for handle in link.handles:
|
||||
rest = below(handle, record.directory)
|
||||
if rest is not None:
|
||||
key = "/".join(p for p in (sub, rest) if p)
|
||||
found.setdefault(key, set()).add(target)
|
||||
linked = True
|
||||
if not linked and record.roms_folder is not None:
|
||||
found.setdefault(sub, set()).add((ROMS_TREE, record.roms_folder))
|
||||
if EXECUTABLE_DIRECTORY in wanted:
|
||||
paths = list(layout)
|
||||
for run in executed:
|
||||
if run.emulator not in names:
|
||||
continue
|
||||
# A program run from a folder outside both trees (RetroBat's
|
||||
# emulators/pico8) binds its folder to no place a pack fills.
|
||||
root, path = run.place or (OUTSIDE_TREE, run.name)
|
||||
folder = path.rsplit("/", 1)[0] if "/" in path else ""
|
||||
for entry_path in paths:
|
||||
if entry_path.rsplit("/", 1)[-1] != run.name:
|
||||
continue
|
||||
sub = entry_path.rsplit("/", 1)[0] if "/" in entry_path else ""
|
||||
found.setdefault(sub, set()).add((root, folder))
|
||||
return {sub: next(iter(places)) for sub, places in found.items() if len(places) == 1}
|
||||
|
||||
|
||||
def bound_path(path: str, bindings: dict[str, Place]) -> Place | None:
|
||||
"""Where a platform holds a layout path, None when no binding covers it.
|
||||
|
||||
The longest subfolder that contains the path wins: a link of
|
||||
kickstarts/ places kickstarts/kick13.rom even when the whole layout is
|
||||
bound elsewhere.
|
||||
"""
|
||||
for sub in sorted(bindings, key=len, reverse=True):
|
||||
if sub and not path.startswith(sub + "/"):
|
||||
continue
|
||||
if record.folder is not None:
|
||||
folders.add(record.folder)
|
||||
continue
|
||||
folders.update(
|
||||
link.folder
|
||||
for link in records
|
||||
if link.folder is not None and record.directory in link.handles
|
||||
)
|
||||
return next(iter(folders)) if len(folders) == 1 else None
|
||||
rest = path[len(sub) + 1:] if sub else path
|
||||
root, folder = bindings[sub]
|
||||
return (root, f"{folder}/{rest}" if folder else rest)
|
||||
return None
|
||||
|
||||
|
||||
def held_record(report: dict, db: dict) -> dict | None:
|
||||
|
||||
+48
-6
@@ -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 sanitize_pack_path
|
||||
from common import ROMS_ROOT, 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,11 +43,24 @@ 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", ""))))
|
||||
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 roms_destination(dest: str) -> str:
|
||||
"""The key of a destination below the roms directory."""
|
||||
return f"{ROMS_KEY}{dest}"
|
||||
|
||||
|
||||
def prefixed_destination(dest: str, prefix: str) -> str:
|
||||
"""A core extra's destination from the pack root.
|
||||
|
||||
@@ -309,9 +322,11 @@ class ExtraResolver:
|
||||
return asset, "release_asset", ""
|
||||
if status in ("not_found", "external", "user_provided") or not local_path:
|
||||
return None, status, ""
|
||||
# The cores reading a moved layout's file are found under the
|
||||
# emulator's own path, not the folder the platform put it in.
|
||||
local_path, disagreement = validated_choice(
|
||||
fe, local_path, self.db, self.validation_index, self.bios_dir, None,
|
||||
dest, self.validation_owners,
|
||||
fe.get("layout_path") or dest, self.validation_owners,
|
||||
)
|
||||
# The platform never reads the bytes, the core does: a copy it turns
|
||||
# away by its own size check is not its file, on any platform.
|
||||
@@ -462,9 +477,10 @@ def unplaceable_extras(
|
||||
placement = ExtraPlacement(config, "", profiles)
|
||||
unplaced: dict[tuple[str, str, str], str] = {}
|
||||
for u in undeclared:
|
||||
if not u.get("in_repo"):
|
||||
if not u.get("in_repo") and not u.get("unplaced"):
|
||||
continue
|
||||
if not placement.slug_based and u.get("root") != "save":
|
||||
if (not placement.slug_based and u.get("root") not in ("save", ROMS_ROOT)
|
||||
and not u.get("unplaced")):
|
||||
continue
|
||||
dest = _extra_dest(u)
|
||||
if not placement.places(u, dest):
|
||||
@@ -550,7 +566,8 @@ def _agnostic_scan_extras(
|
||||
continue
|
||||
# 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):
|
||||
if (read_from_save_dir(f, standalone) or f.get("unplaced")
|
||||
or f.get("root") == ROMS_ROOT):
|
||||
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
|
||||
@@ -1001,6 +1018,9 @@ 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"))
|
||||
self.config = config
|
||||
self.norm_map = (
|
||||
{_norm_system_id(sid): sid for sid in self.platform_systems}
|
||||
@@ -1019,6 +1039,10 @@ class ExtraPlacement:
|
||||
directory and goes below the platform's save folder, which a
|
||||
platform keeping saves beside the content does not have.
|
||||
"""
|
||||
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") == "save":
|
||||
placed = self.save_places(entry, dest)
|
||||
return placed[0] if placed else None
|
||||
@@ -1039,6 +1063,8 @@ class ExtraPlacement:
|
||||
platform that hands each system a save folder of its own goes once
|
||||
below each system that runs the core.
|
||||
"""
|
||||
if entry.get("unplaced"):
|
||||
return []
|
||||
if entry.get("root") == "save":
|
||||
return self.save_places(entry, dest)
|
||||
placed = self.place(entry, dest)
|
||||
@@ -1075,6 +1101,10 @@ class ExtraPlacement:
|
||||
|
||||
def unplaced_reason(self, entry: dict, dest: str) -> str:
|
||||
"""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") == "save":
|
||||
if self.save is None:
|
||||
return "no_save_directory"
|
||||
@@ -1196,6 +1226,12 @@ def _collect_emulator_extras(
|
||||
# user's machine. The mode a file is bundled for was settled
|
||||
# upstream, an entry scoped to another build never gets here.
|
||||
continue
|
||||
if u.get("unplaced"):
|
||||
# Read in a folder outside the BIOS tree: named, held or not,
|
||||
# never written where the emulator does not read it.
|
||||
if unplaceable is not None:
|
||||
unplaceable.append({**u, "unplaceable": u["unplaced"]})
|
||||
continue
|
||||
if not u["in_repo"] and absent is None:
|
||||
continue
|
||||
# For archive entries, use the archive name for resolution
|
||||
@@ -1291,7 +1327,9 @@ def _collect_emulator_extras(
|
||||
# Only duplicate files already covered at another destination.
|
||||
# An unsourceable entry is a file no collection holds: the name
|
||||
# covered elsewhere belongs to another program's file.
|
||||
if fname not in covered_names or f.get("unsourceable"):
|
||||
# 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):
|
||||
continue
|
||||
# Installed with the emulator or by the platform's package,
|
||||
# as the first pass already decided for the same entry.
|
||||
@@ -1530,8 +1568,12 @@ 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
|
||||
if u.get("system_dir"):
|
||||
extra["system_dir"] = u["system_dir"]
|
||||
if u.get("layout_path"):
|
||||
extra["layout_path"] = u["layout_path"]
|
||||
# The archive layout the core opens ranks same-named copies by what
|
||||
# they hold. Without it the first copy indexed won.
|
||||
if u.get("contents"):
|
||||
|
||||
@@ -51,6 +51,7 @@ SOURCE_FILES = (
|
||||
f"{GENERATORS}/OpenMSX.Generator.cs",
|
||||
f"{GENERATORS}/PSXMame.Generator.cs",
|
||||
f"{GENERATORS}/Pcsx2.Generator.cs",
|
||||
f"{GENERATORS}/Pico8.Generator.cs",
|
||||
f"{GENERATORS}/PortsLauncher.Configuration.cs",
|
||||
f"{GENERATORS}/Project64.Generator.cs",
|
||||
f"{GENERATORS}/Raine.Generator.cs",
|
||||
@@ -2979,6 +2980,44 @@ def xroar(sources: dict[str, Source]) -> list[Record]:
|
||||
return _covered(src, records)
|
||||
|
||||
|
||||
def pico8(sources: dict[str, Source]) -> list[Record]:
|
||||
"""The PICO-8 binary the generator runs, from an emulator folder.
|
||||
|
||||
AppConfig.GetFullPath names a folder of the RetroBat install
|
||||
(emulatorLauncher.cfg, else emulators/<key>): the binary is never
|
||||
looked up in the BIOS tree.
|
||||
"""
|
||||
src = sources[f"{GENERATORS}/Pico8.Generator.cs"]
|
||||
folder = src.find(r'string path = AppConfig\.GetFullPath\("(\w+)"\);', "pico8 folder")
|
||||
exe = src.find(
|
||||
r'string exe = Path\.Combine\(path, "([^"]+)"\);', "binary path", folder.end()
|
||||
)
|
||||
check = src.find(r"if \(!File\.Exists\(exe\)\)\s*return null;", "binary check", exe.end())
|
||||
run = src.find(r"FileName = exe,\s*WorkingDirectory = path,", "binary run", check.end())
|
||||
return [
|
||||
_record(
|
||||
"pico8",
|
||||
"standalone",
|
||||
"fixed_name",
|
||||
[exe.group(1)],
|
||||
source_ref(
|
||||
(
|
||||
src.path,
|
||||
[
|
||||
src.lines(folder.start(), folder.end()),
|
||||
src.lines(exe.start(), check.end()),
|
||||
src.lines(run.start(), run.end()),
|
||||
],
|
||||
)
|
||||
),
|
||||
case_insensitive=True,
|
||||
search_dir=f"<AppConfig.GetFullPath(\"{folder.group(1)}\")>",
|
||||
passes_to="exec",
|
||||
fails_when_missing="returns no process when the binary is absent",
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def snes9x(sources: dict[str, Source]) -> list[Record]:
|
||||
src = sources[f"{GENERATORS}/Snes9x.Generator.cs"]
|
||||
entry = src.find(rf'\{{ "([^"]+)", (Path\.Combine\({_BIOS_ROOT}\)) \}}', "bios folder entry")
|
||||
@@ -3247,6 +3286,7 @@ EXTRACTORS: tuple[Callable[[dict[str, Source]], list[Record]], ...] = (
|
||||
option_paths,
|
||||
pcsx2,
|
||||
pcsx2_libretro,
|
||||
pico8,
|
||||
psx_cascades,
|
||||
psxmame,
|
||||
raine,
|
||||
|
||||
@@ -49,9 +49,10 @@ OUTSIDE_BIOS: dict[str, tuple[str, ...]] = {
|
||||
}
|
||||
|
||||
# Packages whose emulator reads its system files below the working directory
|
||||
# the generated launch script enters with pushd, a folder outside $biosdir:
|
||||
# DraStic opens system/<name> under it. Each must still pushd.
|
||||
WORKING_DIRECTORY: tuple[str, ...] = ("drastic",)
|
||||
# the generated launch script enters with pushd: DraStic opens system/<name>
|
||||
# under it, UAE4All2 kickstarts/<name> (its launchDir is getcwd()). Each must
|
||||
# still pushd.
|
||||
WORKING_DIRECTORY: tuple[str, ...] = ("drastic", "uae4all")
|
||||
|
||||
_FUNCTION = re.compile(r"^function\s+([\w.+-]+)\s*\(\)\s*\{\s*$")
|
||||
_ASSIGN = re.compile(r"^(?:local\s+)?([A-Za-z_]\w*)=(.*)$")
|
||||
|
||||
@@ -575,6 +575,47 @@ def armsx2(tree: RepoTree) -> list[Launcher]:
|
||||
|
||||
|
||||
# Start scripts whose ln -s commands put a BIOS path where the emulator reads.
|
||||
def pico8(tree: RepoTree) -> list[Launcher]:
|
||||
"""The PICO-8 binary start_pico8.sh executes, outside /storage/roms/bios.
|
||||
|
||||
The script picks the binary by HW_ARCH, pico8_64 on aarch64 and
|
||||
pico8_dyn on any other, and runs it from GAME_DIR, or from
|
||||
GAME_DIR/${HW_ARCH} when that folder exists.
|
||||
"""
|
||||
path = f"{STANDALONE}/pico-8/scripts/start_pico8.sh"
|
||||
script = Script(path, tree.read(path))
|
||||
game_line, game = script.find(r'^\s*GAME_DIR="([^"$]+)"', "GAME_DIR assignment")
|
||||
case_line, _ = script.find(r"^\s*case \$\{HW_ARCH\} in", "case on HW_ARCH")
|
||||
arch_line, arch = script.find(r"^\s*(\w+)\)", "HW_ARCH branch", case_line + 1)
|
||||
first_line, first = script.find(r'STATIC_BIN="([^"$]+)"', "binary of the first branch", arch_line)
|
||||
other_line, _ = script.find(r"^\s*\*\)", "default HW_ARCH branch", first_line)
|
||||
rest_line, rest = script.find(r'STATIC_BIN="([^"$]+)"', "binary of the default branch", other_line)
|
||||
sub_line, _ = script.find(
|
||||
r'if \[ -d "\$\{GAME_DIR\}/\$\{HW_ARCH\}" \]', "per-architecture launch folder"
|
||||
)
|
||||
end_line, _ = script.find(r"^\s*fi\b", "end of the launch folder test", sub_line)
|
||||
run_line, _ = script.find(r"^\s*\$\{LAUNCH_DIR\}/\$\{STATIC_BIN\} ", "binary run")
|
||||
return [
|
||||
Launcher(
|
||||
emulator="pico-8",
|
||||
build="standalone",
|
||||
kind="fixed_name",
|
||||
names=[first.group(1), rest.group(1)],
|
||||
source_ref=source_ref([(path, {
|
||||
game_line, case_line, arch_line, first_line, other_line, rest_line,
|
||||
*range(sub_line, end_line + 1), run_line,
|
||||
})]),
|
||||
search_dir=game.group(1).rstrip("/"),
|
||||
passes_to="exec",
|
||||
fails_when_missing=False,
|
||||
depends_on_option=(
|
||||
f"HW_ARCH {arch.group(1)} runs {first.group(1)}, any other {rest.group(1)}, "
|
||||
f"from {game.group(1).rstrip('/')}/${{HW_ARCH}} when that folder exists"
|
||||
),
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
LINK_SCRIPTS = (
|
||||
("drastic", f"{STANDALONE}/drastic-sa/scripts/start_drastic.sh"),
|
||||
("dolphin", f"{STANDALONE}/dolphin-sa/scripts/start_dolphin_gc.sh"),
|
||||
@@ -679,6 +720,7 @@ EXTRACTORS: tuple[Callable[[RepoTree], list[Launcher]], ...] = (
|
||||
yabasanshiro,
|
||||
duckstation,
|
||||
armsx2,
|
||||
pico8,
|
||||
patched_directories,
|
||||
links,
|
||||
)
|
||||
|
||||
+2
-1
@@ -228,7 +228,7 @@ def profile_claims(
|
||||
# outside the BIOS tree, where no platform declares a file.
|
||||
if outside_gap_scope(entry, is_standalone) or read_from_save_dir(
|
||||
entry, is_standalone
|
||||
):
|
||||
) or entry.get("unplaced"):
|
||||
continue
|
||||
# A member of a romset is not a file at the BIOS root: the archive
|
||||
# holding it occupies the destination (_archive_claims). Claiming
|
||||
@@ -306,6 +306,7 @@ def _archive_claims(
|
||||
if isinstance(entry, dict)
|
||||
and not outside_gap_scope(entry, is_standalone)
|
||||
and not read_from_save_dir(entry, is_standalone)
|
||||
and not entry.get("unplaced")
|
||||
]
|
||||
prefix = profile.get("archive_prefix", "")
|
||||
claims: list[Claim] = []
|
||||
|
||||
+9
-7
@@ -427,13 +427,15 @@ def _has_exploitable_data(entry: dict) -> bool:
|
||||
|
||||
|
||||
def _system_dir_files(profile: dict, standalone: bool) -> list[dict]:
|
||||
"""Entries of the build the platform runs, read from the system directory."""
|
||||
return list(
|
||||
filter(
|
||||
read_from_system_dir,
|
||||
filter_files_by_mode(profile.get("files", []), standalone=standalone),
|
||||
)
|
||||
)
|
||||
"""Entries of the build the platform runs, read from the system directory.
|
||||
|
||||
An entry the platform's launchers leave outside the BIOS tree
|
||||
(``unplaced``) is not: the platform has no file of its own there.
|
||||
"""
|
||||
return [
|
||||
f for f in filter_files_by_mode(profile.get("files", []), standalone=standalone)
|
||||
if read_from_system_dir(f) and not f.get("unplaced")
|
||||
]
|
||||
|
||||
|
||||
def _archives_not_members(files: list[dict]) -> list[dict]:
|
||||
|
||||
+23
-3
@@ -592,7 +592,10 @@ def find_undeclared_files(
|
||||
# Below the save directory each core is handed a folder
|
||||
# of its own: two cores reading hiscore.dat there read
|
||||
# two files.
|
||||
("save", core_save_folder(profile)) if save_root else None,
|
||||
("save", core_save_folder(profile)) if save_root
|
||||
else ("roms",) if f.get("root") == "roms"
|
||||
# In the emulator's own folder: no other core reads it.
|
||||
else ("emulator", emu_name) if f.get("unplaced") else None,
|
||||
)
|
||||
if not fname or unread_on(f, config.get("_key", "")):
|
||||
continue
|
||||
@@ -600,9 +603,11 @@ def find_undeclared_files(
|
||||
duplicate = seen_key in seen_files
|
||||
if duplicate and (first is None or first["profile"] == emu_name):
|
||||
continue
|
||||
# Nor does it answer for one read below the roms directory.
|
||||
verdict = _candidate_verdict(
|
||||
f, fname, is_standalone, include_all,
|
||||
{} if save_root else declared_names, dest, folded_slots,
|
||||
{} if save_root or f.get("root") == "roms" else declared_names,
|
||||
dest, folded_slots,
|
||||
)
|
||||
if verdict == "settled":
|
||||
seen_files.add(seen_key)
|
||||
@@ -759,7 +764,14 @@ def find_undeclared_files(
|
||||
members = with_archive_members(f, profile.get("files", [])).get("members")
|
||||
if members:
|
||||
entry["members"] = members
|
||||
if launcher_layer and not save_root:
|
||||
if f.get("layout_path"):
|
||||
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 launcher_layer and not save_root:
|
||||
launcher_candidates.append((emu_name, is_standalone, f, entry))
|
||||
if duplicate:
|
||||
first.setdefault("other_claims", []).append(entry)
|
||||
@@ -867,6 +879,8 @@ 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_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",
|
||||
@@ -890,6 +904,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":
|
||||
# The pack is extracted into the BIOS folder: the installer
|
||||
# writes this file into the roms folder, the pack cannot.
|
||||
u["in_pack"] = False
|
||||
u["omitted"] = "read below the roms folder, which only the installer reaches"
|
||||
continue
|
||||
if reason:
|
||||
u["in_pack"] = False
|
||||
u["omitted"] = _UNPLACED_REASONS[reason]
|
||||
|
||||
Reference in new issue
Block a user