mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-25 05:09:03 -05:00
fix: print each app's data folder per system
This commit is contained in:
1 parent
0e5aa804a7
commit
1c3db5b4d8
2 files changed
+383
-44
No files matched your search
+379
-43
@@ -55,6 +55,7 @@ from common import (
|
||||
resolution_is_hash_exact,
|
||||
resolve_local_file,
|
||||
resolve_platform_cores,
|
||||
runs_standalone,
|
||||
unique_emulator_profiles,
|
||||
yaml_load,
|
||||
)
|
||||
@@ -7380,11 +7381,13 @@ def _render_emulator_file(
|
||||
anchor: str = "",
|
||||
owner: str = "",
|
||||
alias: str = "",
|
||||
data_href: str = "#data-folder",
|
||||
) -> list[str]:
|
||||
"""Render one row of an emulator profile's file table.
|
||||
|
||||
`anchor` is the card's id on the page, so one file can be cited by URL,
|
||||
and `alias` a second id it answers to.
|
||||
and `alias` a second id it answers to. `data_href` links the data
|
||||
folder an app reads the entry under, from the page the card lands on.
|
||||
|
||||
Split out of generate_emulator_page, where it was a 228-line loop body
|
||||
carrying most of that function's branching: thirty-odd optional fields,
|
||||
@@ -7436,18 +7439,23 @@ def _render_emulator_file(
|
||||
lines.append(_card_title(f"{marker}{label} {badge_str}", anchor, weight=5))
|
||||
lines.append("")
|
||||
if desc:
|
||||
lines.extend([_escape_tags(str(desc)), ""])
|
||||
lines.extend([data_root_words(_escape_tags(str(desc)), data_href), ""])
|
||||
body_start = len(lines)
|
||||
|
||||
details = []
|
||||
if fpath and fpath != fname:
|
||||
details.append(f"Path: `{fpath}`")
|
||||
# Where the app reads it under its data folder, said beside the path:
|
||||
# Ryujinx's card read "Path: system/prod.keys" with no root.
|
||||
place = standalone_place(owner, profile, f) if owner else ""
|
||||
under = f", in [the data folder]({data_href})"
|
||||
standalone_path = f.get("standalone_path", "")
|
||||
if (fpath and fpath != fname) or (place and place == fpath):
|
||||
details.append(f"Path: `{fpath}`" + (under if place and place == fpath else ""))
|
||||
if standalone_path and standalone_path != fpath:
|
||||
# The standalone build reads it elsewhere: MesenCE's dsp1.rom goes
|
||||
# under Firmware/, and a page naming only the core's path sent its
|
||||
# users to the wrong folder.
|
||||
details.append(f"Standalone path: `{standalone_path}`")
|
||||
details.append(f"Standalone path: `{standalone_path}`"
|
||||
+ (under if place == standalone_path else ""))
|
||||
read_from = _load_from_text(f.get("load_from"))
|
||||
if read_from:
|
||||
details.append(f"Read from: {read_from}")
|
||||
@@ -7571,7 +7579,7 @@ def _render_emulator_file(
|
||||
for d in details:
|
||||
lines.append(f"- {d}")
|
||||
if fnote:
|
||||
lines.append(f"- {_escape_tags(link_prose_citations(profile, str(fnote)))}")
|
||||
lines.append(f"- {data_root_words(_escape_tags(link_prose_citations(profile, str(fnote))), data_href)}")
|
||||
if not in_repo and f.get("unsourceable"):
|
||||
# The badge alone sent readers after a dump that cannot exist, and the
|
||||
# reason was only on the gaps page, which the card did not link.
|
||||
@@ -7720,6 +7728,14 @@ def _render_metadata_rows(profile: dict) -> list[str]:
|
||||
lines.append(f"| {label} | [{val}]({val}) |")
|
||||
else:
|
||||
lines.append(f"| {label} | {val} |")
|
||||
roots = profile.get("data_root")
|
||||
if isinstance(roots, dict):
|
||||
# The folder per system, for the reader who checks the profile.
|
||||
for key in ("windows", "macos", "linux", "android", "ios"):
|
||||
root = roots.get(key)
|
||||
if isinstance(root, dict) and root.get("path"):
|
||||
lines.append(f"| Data folder ({_OS_LABELS[key]}) | "
|
||||
f"{_under_root(str(root['path']), '')} |")
|
||||
if profile.get("source_ref"):
|
||||
lines.append(
|
||||
f"| Source ref | {_source_ref_markdown(profile, profile['source_ref'])} |"
|
||||
@@ -7727,10 +7743,18 @@ def _render_metadata_rows(profile: dict) -> list[str]:
|
||||
return lines
|
||||
|
||||
|
||||
def install_commands(args: str = "", indent: str = "") -> list[str]:
|
||||
def install_commands(args: str = "", indent: str = "",
|
||||
oses: tuple[str, ...] | None = None) -> list[str]:
|
||||
"""The same install for each place a user runs it: one generator for the
|
||||
download page and the platform, system and emulator pages, so none of
|
||||
them loses the Android or Windows form."""
|
||||
them loses the Android or Windows form.
|
||||
|
||||
``oses`` names the terminals (keys of TERMINALS) the command runs in,
|
||||
written as the synced OS tabs: MAME4droid runs on Android only, and its
|
||||
page offered a PowerShell command. None keeps the three blocks.
|
||||
"""
|
||||
if oses is not None:
|
||||
return terminal_tabs(oses, dict.fromkeys(oses, args), indent)
|
||||
lines = []
|
||||
for label, shell in (
|
||||
("Linux, macOS, Steam Deck:", "sh"),
|
||||
@@ -7741,29 +7765,294 @@ def install_commands(args: str = "", indent: str = "") -> list[str]:
|
||||
return lines
|
||||
|
||||
|
||||
@functools.cache
|
||||
def _installer() -> object:
|
||||
"""install.py, read as a module: the names `--emulator` accepts are its
|
||||
own (`emulator_names`)."""
|
||||
import importlib.util # noqa: PLC0415
|
||||
|
||||
spec = importlib.util.spec_from_file_location("install", _REPO_ROOT / "install.py")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
@functools.cache
|
||||
def _published_emulator_names() -> dict[str, dict[str, str]]:
|
||||
"""Per published manifest, every name `--emulator` accepts there and the
|
||||
profile it stands for, as `install.py --list-emulators` prints them."""
|
||||
installer = _installer()
|
||||
names: dict[str, dict[str, str]] = {}
|
||||
for path in sorted((_REPO_ROOT / "install").glob("*.json")):
|
||||
manifest = json.loads(path.read_text(encoding="utf-8"))
|
||||
names[path.stem] = installer.emulator_names(
|
||||
manifest, manifest.get("files") or [], manifest.get("omitted_files") or [])
|
||||
return names
|
||||
|
||||
|
||||
# Where a setup's command runs, in the order the tabs show them.
|
||||
_COMMAND_TERMINALS = ("windows", "macos", "linux", "steamdeck", "android", "device")
|
||||
|
||||
# The terminals an app's data_root key covers: the Deck runs the Linux build.
|
||||
_DATA_ROOT_TERMINALS = {
|
||||
"windows": ("windows",), "macos": ("macos",), "linux": ("linux", "steamdeck"),
|
||||
"android": ("android",), "ios": (),
|
||||
}
|
||||
|
||||
|
||||
def emulator_install_route(name: str, profile: dict) -> tuple[str, list[str], tuple[str, ...]]:
|
||||
"""The name `--emulator` takes for this profile, the setups the command
|
||||
fills, and the terminals it runs in, or ("", [], ()) when none works.
|
||||
|
||||
A name works where a published manifest maps it to this profile: 215
|
||||
pages printed a command every manifest refused. A setup counts when the
|
||||
installer finds it without `--dest`. A standalone app is reached only
|
||||
through a setup that launches it as an app (its standalone_cores), on
|
||||
the systems both run on: RetroArch's list names AetherSX2, and the
|
||||
command filled RetroArch's system folder, which the app never reads.
|
||||
"""
|
||||
if profile.get("type") in ("alias", "launcher", "test"):
|
||||
return "", [], ()
|
||||
if not [f for f in profile.get("files") or [] if isinstance(f, dict) and not comes_with_app(f)]:
|
||||
return "", [], ()
|
||||
standalone = profile.get("type") == "standalone"
|
||||
accepted: dict[str, set[str]] = {}
|
||||
for platform, names in _published_emulator_names().items():
|
||||
if platform not in GUIDES:
|
||||
continue
|
||||
if standalone and not runs_standalone(name, profile, _standalone_cores(platform)):
|
||||
continue
|
||||
if any("--dest" in args for args in platform_args(platform, GUIDES[platform]).values()):
|
||||
continue
|
||||
own = {n for n, target in names.items() if target == name}
|
||||
if own:
|
||||
accepted[platform] = own
|
||||
if not accepted:
|
||||
return "", [], ()
|
||||
# The profile's own name where a setup takes it, else the alias most
|
||||
# setups take: the one command must work on every setup it names.
|
||||
candidates = set().union(*accepted.values())
|
||||
command = min(candidates, key=lambda n: (
|
||||
-sum(n in own for own in accepted.values()), n != name, n))
|
||||
order = list(GUIDES)
|
||||
setups = sorted((p for p, own in accepted.items() if command in own), key=order.index)
|
||||
terminals = {key for setup in setups for key in GUIDES[setup].run}
|
||||
if isinstance(profile.get("data_root"), dict):
|
||||
# Only the systems the program is built for (Cemu has no Android
|
||||
# build). A device setup runs it itself: its terminal stays.
|
||||
terminals &= {"device", *(t for key in profile["data_root"]
|
||||
for t in _DATA_ROOT_TERMINALS.get(key, ()))}
|
||||
oses = tuple(key for key in _COMMAND_TERMINALS if key in terminals)
|
||||
if not oses:
|
||||
return "", [], ()
|
||||
return command, setups, oses
|
||||
|
||||
|
||||
@functools.cache
|
||||
def _standalone_cores(platform: str) -> frozenset[str]:
|
||||
"""The emulators a setup launches as apps, by its platform file."""
|
||||
config = load_platform_config(platform, str(_REPO_ROOT / "platforms"))
|
||||
return frozenset(str(c) for c in config.get("standalone_cores") or [])
|
||||
|
||||
|
||||
def _said_or(names: list[str]) -> str:
|
||||
return names[0] if len(names) == 1 else ", ".join(names[:-1]) + f" or {names[-1]}"
|
||||
|
||||
|
||||
def _install_block(name: str, profile: dict) -> list[str]:
|
||||
"""One command that installs this emulator's files where it reads them.
|
||||
|
||||
The installer finds the platform and its BIOS folder, then keeps what
|
||||
this emulator reads there: its own files and the platform's files of
|
||||
the systems it runs. Each file is also linked one by one below.
|
||||
The installer finds the setup and its BIOS folder, then keeps what this
|
||||
emulator reads there: its own files and the setup's files of the
|
||||
systems it runs. Written only where a published manifest takes the
|
||||
name, in the terminals of the setups that run it. `--emulator` turns off
|
||||
the pass that fills a lone app's folders (install.py, `narrowing` in
|
||||
main), so an app on its own is named with the setup that serves it.
|
||||
"""
|
||||
if profile.get("type") in ("alias", "launcher", "test"):
|
||||
return []
|
||||
if profile.get("type") == "standalone" and any_name_bios(profile) is not None:
|
||||
# `--emulator` installs into a platform's BIOS folder and turns off
|
||||
# the pass that fills standalone emulator folders (install.py,
|
||||
# `narrowing` in main): an app on its own reported "No supported
|
||||
# platform detected". The download step of the intro replaces it.
|
||||
command, setups, oses = emulator_install_route(name, profile)
|
||||
if not command:
|
||||
return []
|
||||
emu = str(profile.get("emulator") or name)
|
||||
shown = [_platform_display(setup) for setup in setups]
|
||||
if profile.get("type") == "standalone":
|
||||
lead = (f"With {_said_or(shown)}: the installer finds {_said_or(shown)} and its "
|
||||
f"BIOS folder, then downloads only the files {emu} reads there and checks "
|
||||
"each one.")
|
||||
else:
|
||||
lead = ("The installer finds your setup and its BIOS folder, then downloads "
|
||||
"only the files this emulator reads there and checks each one.")
|
||||
if profile.get("type") == "standalone + libretro":
|
||||
# The command fills a setup, never the app's own folder.
|
||||
where = (_said_or(shown) if len(shown) <= 3
|
||||
else f"{shown[0]}, {shown[1]} or another setup")
|
||||
lead = f"For {emu} inside {where}. {lead}"
|
||||
return search_excluded(markup.details("tip", "Install this emulator's files", [
|
||||
"The installer finds your platform and its BIOS folder, then downloads "
|
||||
"only the files this emulator reads there and checks each one.",
|
||||
lead,
|
||||
"",
|
||||
*install_commands(f"--emulator {name}"),
|
||||
*install_commands(f"--emulator {command}", oses=oses),
|
||||
]))
|
||||
|
||||
|
||||
# Entries whose folder the app opens is not data_root joined to their
|
||||
# standalone_path (or path): the pack builder still places them by that
|
||||
# field, so the joined path names a place the app never reads. Their page
|
||||
# gives the data folder alone. A row names the profile, the entries (a name,
|
||||
# a path prefix ending in "/", or "*" for every file) and, when the rule
|
||||
# holds on some systems only, the data_root keys it covers.
|
||||
FOLDER_ONLY: tuple[tuple[str, tuple[str, ...], tuple[str, ...], str], ...] = (
|
||||
("amiberry", ("*",), (),
|
||||
"the app reads every ROM, Kickstarts included, under ROMs/ of its folder"),
|
||||
("fsuae", ("fsuae/Kickstarts/",), (),
|
||||
"the app reads Kickstarts from ROMs/Kickstart/ of its folder"),
|
||||
("citra", ("Citra/sysdata/",), (),
|
||||
"the app reads its keys and shared fonts from sysdata/ of its folder"),
|
||||
("ymir", ("*",), (),
|
||||
"the app reads its IPL, cartridge and CD-block ROMs from roms/ipl/, roms/cart/ and roms/cdb/"),
|
||||
("mame", ("*",), (),
|
||||
"the app reads every set under roms/ of its working directory"),
|
||||
("eka2l1", ("data/",), ("ios",),
|
||||
"on iOS the data folder is the data-storage folder itself, so data/ would be one level too deep"),
|
||||
("aethersx2", ("resources/", "eeprom.dat", "flash.dat"), (),
|
||||
"resources sit beside the program, and eeprom.dat and flash.dat in the working directory"),
|
||||
("cemu", ("resources/sharedFonts/",), (),
|
||||
"the shared fonts are read from the program's data path"),
|
||||
("cxbx-reloaded", ("hlsl/",), (),
|
||||
"the shaders are read from beside the program"),
|
||||
("ppsspp-1.5.4", ("ppsspp-1.5.4/assets/",), (),
|
||||
"the assets tree is read from beside the program"),
|
||||
("yaps2", ("resources/",), (),
|
||||
"resources are read from beside the program"),
|
||||
("hatari", ("tos.img", "tos.sym"), (),
|
||||
"the TOS image is read from the install data directory"),
|
||||
("armsx2", ("proverb.zip",), ("android",),
|
||||
"the Android app keeps the arcade boot programs in its internal storage"),
|
||||
("armsx2", ("eeprom.dat", "flash.dat"), (),
|
||||
"eeprom.dat and flash.dat are read from the working directory"),
|
||||
("kenji-nx", ("drivers/",), (),
|
||||
"a driver archive is unpacked under drivers/, a layout not confirmed in the source"),
|
||||
)
|
||||
|
||||
|
||||
def folder_only_matches(pattern: str, entry: dict) -> bool:
|
||||
"""Whether a FOLDER_ONLY pattern names this entry."""
|
||||
if pattern == "*":
|
||||
return True
|
||||
if pattern.endswith("/"):
|
||||
return any(str(entry.get(key) or "").startswith(pattern)
|
||||
for key in ("path", "standalone_path"))
|
||||
return entry.get("name") == pattern
|
||||
|
||||
|
||||
def _folder_only(name: str, entry: dict) -> set[str]:
|
||||
"""The data_root keys on which this entry's joined path is not the app's
|
||||
folder: every key ({"*"}), some, or none."""
|
||||
keys: set[str] = set()
|
||||
for profile, patterns, only, _reason in FOLDER_ONLY:
|
||||
if profile == name and any(folder_only_matches(p, entry) for p in patterns):
|
||||
keys |= set(only) or {"*"}
|
||||
return keys
|
||||
|
||||
|
||||
def standalone_place(name: str, profile: dict, entry: dict, key: str = "") -> str:
|
||||
"""Where under the data folder the app reads this entry, or "".
|
||||
|
||||
Only what the app opens there: a lone app's ``standalone_path`` or
|
||||
``path``, a dual profile's ``standalone_path`` or the ``path`` of an
|
||||
entry its standalone build alone reads. Nothing for an entry found by
|
||||
name, one the app ships, one the libretro core alone reads, or one
|
||||
FOLDER_ONLY lists (on ``key``, or on any key when ``key`` is empty).
|
||||
"""
|
||||
if not isinstance(entry, dict) or not profile.get("data_root"):
|
||||
return ""
|
||||
mode = entry.get("mode")
|
||||
if mode == "libretro" or (entry.get("bundled") is True and mode in (None, "standalone")):
|
||||
return ""
|
||||
excluded = _folder_only(name, entry)
|
||||
if "*" in excluded or (key in excluded if key else excluded):
|
||||
return ""
|
||||
if profile.get("type") == "standalone" or mode == "standalone":
|
||||
return str(entry.get("standalone_path") or entry.get("path") or "")
|
||||
return str(entry.get("standalone_path") or "")
|
||||
|
||||
|
||||
_OS_LABELS = {"windows": "Windows", "macos": "macOS", "linux": "Linux",
|
||||
"android": "Android", "ios": "iOS"}
|
||||
|
||||
|
||||
def _under_root(root: str, folder: str) -> str:
|
||||
"""``root`` with ``folder`` below it, as code: each alternative of
|
||||
"$XDG_CONFIG_HOME/x, else ~/.config/x", and a root written in words
|
||||
("the folder picked in the setup wizard") kept as words."""
|
||||
folder = folder.strip("/")
|
||||
shown = []
|
||||
for alternative in root.split(", else "):
|
||||
alternative = alternative.strip()
|
||||
if " " in alternative and "/" not in alternative and not alternative.startswith("<"):
|
||||
shown.append(f"{_code(folder + '/')} in {_escape_tags(alternative)}"
|
||||
if folder else _escape_tags(alternative))
|
||||
continue
|
||||
joined = alternative.rstrip("/") + (f"/{folder}" if folder else "") + "/"
|
||||
shown.append(_code(joined))
|
||||
return ", else ".join(shown)
|
||||
|
||||
|
||||
def _data_root_folder(name: str, profile: dict, key: str, entries: list[dict]) -> str:
|
||||
"""The one folder under the root that every entry the app reads sits
|
||||
in, or "": VICE places 3 of its 322 files in DRIVES/, and naming that
|
||||
folder would send the other 319 there."""
|
||||
folders = set()
|
||||
for f in entries:
|
||||
mode = f.get("mode")
|
||||
if mode == "libretro" or (f.get("bundled") is True and mode in (None, "standalone")):
|
||||
continue
|
||||
place = standalone_place(name, profile, f, key)
|
||||
if not place:
|
||||
return ""
|
||||
folders.add(place.rpartition("/")[0])
|
||||
return folders.pop() if len(folders) == 1 else ""
|
||||
|
||||
|
||||
def data_folder_block(name: str, profile: dict, entries: list[dict] | None = None) -> list[str]:
|
||||
"""The app's data folder, one tab per system it runs on, each with the
|
||||
code that names it and its note: 61 profiles cite it and no page showed
|
||||
it. ``entries`` are the files the folder is for: when they all sit in
|
||||
one folder below the root, the tab names that folder."""
|
||||
roots = profile.get("data_root") or {}
|
||||
if not isinstance(roots, dict) or not roots:
|
||||
return []
|
||||
if entries is None:
|
||||
entries = [f for f in profile.get("files") or [] if isinstance(f, dict)]
|
||||
items = []
|
||||
for key in ("windows", "macos", "linux", "android", "ios"):
|
||||
root = roots.get(key)
|
||||
if not isinstance(root, dict) or not root.get("path"):
|
||||
continue
|
||||
path = str(root["path"])
|
||||
line = _under_root(path, _data_root_folder(name, profile, key, entries))
|
||||
if key == "android" and path.startswith("Android/data/"):
|
||||
line += ", on the internal storage."
|
||||
body = [line]
|
||||
note = " ".join(str(root.get("note") or "").split()).rstrip(".")
|
||||
if note:
|
||||
# "the folder" reads as a sentence, "portable.ini" keeps its case.
|
||||
first = note.split(" ", 1)[0]
|
||||
if first.isalpha() and first.islower():
|
||||
note = note[:1].upper() + note[1:]
|
||||
body += ["", _escape_tags(note) + "."]
|
||||
body += ["", f"Source: {_source_ref_markdown(profile, str(root['source_ref']))}"]
|
||||
items.append((_OS_LABELS[key], body))
|
||||
if len(items) == 1:
|
||||
label, body = items[0]
|
||||
return [f"On {label}: {body[0]}", *body[1:], ""]
|
||||
return markup.tabs(items)
|
||||
|
||||
|
||||
def data_root_words(text: str, href: str = "#data-folder") -> str:
|
||||
"""Prose that names the field data_root, linked to the folder it holds:
|
||||
ten pages told readers "given in data_root"."""
|
||||
return re.sub(r"\bdata_root\b", f"[the data folder]({href})", text)
|
||||
|
||||
|
||||
def _platform_install_block(name: str, display: str, on_card: bool = False) -> list[str]:
|
||||
"""The platform's pack, installed file by file: what the ZIP holds, with
|
||||
resume, verification and only what is missing.
|
||||
@@ -7922,8 +8211,10 @@ def _render_structured_blocks(profile: dict) -> list[str]:
|
||||
body: list[str] = []
|
||||
_render_yaml_value(body, val, indent=0,
|
||||
field_text=_structured_field_text(profile))
|
||||
# Closed: raw profile data, below the files (Cemu's firmware
|
||||
# titles opened the page above them).
|
||||
block = markup.details(
|
||||
"abstract", label, [_escape_tags(line) for line in body], opened=True)
|
||||
"abstract", label, [_escape_tags(line) for line in body])
|
||||
# A block listing hashes is the lookup's, as a card's hash lines are:
|
||||
# izapple2's ROM table put "md5" 59 times in the search, above the
|
||||
# page that explains MD5 checks.
|
||||
@@ -8016,6 +8307,12 @@ def _emulator_file_summary(files: list[dict]) -> list[str]:
|
||||
if sum(len(group) for group in slots) != len(slots):
|
||||
total += f" ({sum(len(group) for group in slots):,} accepted versions)"
|
||||
line = f"{total}: {req_count:,} required, {len(slots) - req_count:,} optional."
|
||||
if _regional_choice([f for f in files if f.get("required") is True and not comes_with_app(f)]):
|
||||
# DuckStation takes the BIOS of the game's region: "3 required"
|
||||
# read as "get all three".
|
||||
line = (f"{total}. One of {req_count:,} is required, for your games' region. "
|
||||
f"{len(slots) - req_count:,} {'is' if len(slots) - req_count == 1 else 'are'} "
|
||||
"optional.")
|
||||
if hle_count:
|
||||
line += f" {hle_count:,} {'has' if hle_count == 1 else 'have'} an HLE fallback."
|
||||
if shipped:
|
||||
@@ -8322,44 +8619,77 @@ def _any_name_folder(profile: dict, entry: dict) -> str:
|
||||
return ""
|
||||
if profile.get("type") == "standalone":
|
||||
# A bare `bios/` is a folder inside the app's own data folder.
|
||||
return f"the {_code(folder.strip('/'))} folder inside the app's own data folder"
|
||||
own = ("[the app's data folder](#data-folder)" if profile.get("data_root")
|
||||
else "the app's own data folder")
|
||||
return f"the {_code(folder.strip('/'))} folder inside {own}"
|
||||
return f"{_code(folder)}, inside your BIOS folder"
|
||||
|
||||
|
||||
def _any_name_steps(emu_name: str, profile: dict, entry: dict, href: str) -> str:
|
||||
def _any_name_steps(emu_name: str, profile: dict, entry: dict, href: str,
|
||||
name: str = "") -> str:
|
||||
"""The intro of an emulator that takes one BIOS of any name: get one,
|
||||
put it in its folder, any name works."""
|
||||
put it in its folder, any name works.
|
||||
|
||||
An app whose profile names its data folder gets that folder, one tab
|
||||
per system it runs on: AetherSX2's step said "inside the app's own
|
||||
data folder" while the profile cites Android/data/xyz.aethersx2.android.
|
||||
"""
|
||||
system = system_name((profile.get("systems") or [""])[0])
|
||||
name = str(entry.get("name") or "")
|
||||
if _named_example(name) and href:
|
||||
get = f"Download [{_code(name)}]({href})."
|
||||
example = str(entry.get("name") or "")
|
||||
if _named_example(example) and href:
|
||||
get = f"Download [{_code(example)}]({href})."
|
||||
else:
|
||||
get = f"Get one from the {_named_system_link(profile['systems'][0], '../')} files."
|
||||
steps = [get]
|
||||
folder = _any_name_folder(profile, entry)
|
||||
if folder:
|
||||
steps.append(f"Put it in {folder}.")
|
||||
folders: list[str] = []
|
||||
if profile.get("type") == "standalone" and standalone_place(name, profile, entry):
|
||||
steps.append("Put it in this folder:")
|
||||
folders = ['<div id="data-folder" class="rb-anchor"></div>', "",
|
||||
*data_folder_block(name, profile, [entry])]
|
||||
else:
|
||||
folder = _any_name_folder(profile, entry)
|
||||
if folder:
|
||||
steps.append(f"Put it in {folder}.")
|
||||
folders = _data_folder_intro(name, profile)
|
||||
reader = "The app" if profile.get("type") == "standalone" else "The emulator"
|
||||
return "\n".join([
|
||||
f"{emu_name} needs one {system} BIOS file, any region.",
|
||||
"",
|
||||
*(f"{number}. {step}" for number, step in enumerate(steps, 1)),
|
||||
"",
|
||||
*folders,
|
||||
*([""] if folders and folders[-1] else []),
|
||||
f"Any file name works. {reader} reads what the file holds.",
|
||||
])
|
||||
|
||||
|
||||
def _emulator_intro(emu_name: str, profile: dict, href: str = "") -> str:
|
||||
def _data_folder_intro(name: str, profile: dict) -> list[str]:
|
||||
"""Where the app keeps its files, under the intro of a profile that
|
||||
names its data folder and takes more than one BIOS of any name."""
|
||||
if not profile.get("data_root"):
|
||||
return []
|
||||
files = [f for f in profile.get("files") or [] if isinstance(f, dict)]
|
||||
placed = [f for f in files if standalone_place(name, profile, f)]
|
||||
subject = ("The app" if profile.get("type") == "standalone"
|
||||
else "Run on its own, the app")
|
||||
lead = (f"{subject} reads its files from this folder:" if placed
|
||||
else f"{subject} keeps its data in this folder:")
|
||||
return ['<div id="data-folder" class="rb-anchor"></div>', "", lead, "",
|
||||
*data_folder_block(name, profile, files)]
|
||||
|
||||
|
||||
def _emulator_intro(emu_name: str, profile: dict, href: str = "", name: str = "") -> str:
|
||||
"""What the emulator needs, in plain words, from fields every profile has.
|
||||
|
||||
The page opened on a twelve-row table: "Type", "Version", "Checked by".
|
||||
DuckStation needs one PlayStation BIOS of any name, and the reader had
|
||||
to infer it from the "Files found by" row. ``href`` links the
|
||||
collection's copy of the one BIOS an emulator takes under any name.
|
||||
collection's copy of the one BIOS an emulator takes under any name,
|
||||
and ``name`` is the profile's key.
|
||||
"""
|
||||
entry = any_name_bios(profile)
|
||||
if entry is not None:
|
||||
return _any_name_steps(emu_name, profile, entry, href)
|
||||
return _any_name_steps(emu_name, profile, entry, href, name)
|
||||
names: list[str] = []
|
||||
for sys_id in profile.get("systems") or []:
|
||||
label = system_name(sys_id)
|
||||
@@ -8395,7 +8725,8 @@ def _emulator_intro(emu_name: str, profile: dict, href: str = "") -> str:
|
||||
sentences.append("It recognises a file by its contents, so any file name works.")
|
||||
if any(f.get("source_ref") for f in files):
|
||||
sentences.append("Each file below cites the line of source code that reads it.")
|
||||
return " ".join(sentences)
|
||||
folder = _data_folder_intro(name, profile) if name else []
|
||||
return "\n".join([" ".join(sentences), "", *folder]).strip("\n")
|
||||
|
||||
|
||||
def _regional_choice(required: list[dict], most: int = 6) -> str:
|
||||
@@ -8467,7 +8798,7 @@ def emulator_pages(
|
||||
part_pages: dict[str, str] = {}
|
||||
one_bios = any_name_bios(profile)
|
||||
intro = _emulator_intro(emu_name, profile, _collection_card_href(
|
||||
one_bios, consumers, name) if one_bios else "")
|
||||
one_bios, consumers, name) if one_bios else "", name)
|
||||
romset = emulator_romset(profile)
|
||||
lines = [
|
||||
f"# {emu_name}",
|
||||
@@ -8565,10 +8896,9 @@ def emulator_pages(
|
||||
|
||||
lines.extend(_render_platform_details(profile))
|
||||
|
||||
lines.extend(_render_structured_blocks(profile))
|
||||
|
||||
if notes:
|
||||
body = _admonition_body(link_prose_citations(profile, notes)).replace("\n ", "\n")
|
||||
body = _admonition_body(data_root_words(link_prose_citations(profile, notes))
|
||||
).replace("\n ", "\n")
|
||||
lines.extend(markup.details("note", "Technical notes", body.split("\n")))
|
||||
|
||||
if not files:
|
||||
@@ -8585,11 +8915,13 @@ def emulator_pages(
|
||||
|
||||
anchors = emulator_file_anchors(files)
|
||||
aliases = emulator_file_aliases(files, anchors)
|
||||
parts = emulator_parts(name, files)
|
||||
data_href = f"{name}.md#data-folder" if parts else "#data-folder"
|
||||
cards = [
|
||||
_render_emulator_file(f, profile, consumers, files, _file_available, anchor, name, alias)
|
||||
_render_emulator_file(f, profile, consumers, files, _file_available, anchor, name,
|
||||
alias, data_href)
|
||||
for f, anchor, alias in zip(files, anchors, aliases)
|
||||
]
|
||||
parts = emulator_parts(name, files)
|
||||
if not parts:
|
||||
order = _display_order(files)
|
||||
indexed = len(files) >= EMULATOR_SUMMARY_MIN
|
||||
@@ -8614,6 +8946,10 @@ def emulator_pages(
|
||||
name, emu_name, number, parts, [cards[i] for i in entries],
|
||||
[files[i] for i in entries])
|
||||
|
||||
blocks = _render_structured_blocks(profile)
|
||||
if blocks:
|
||||
lines.extend(["## Profile data", "", *blocks])
|
||||
|
||||
if data_dirs:
|
||||
# The member tables are a file list with hashes: out of the search,
|
||||
# dolphin, primehack and ishiiruka each put 210 KB of it there.
|
||||
@@ -8747,8 +9083,8 @@ def _emulator_part_page(
|
||||
for n, (stem, _e) in enumerate(parts, 1)], number - 1),
|
||||
f"# {emu_name} files, part {number} of {len(parts)}",
|
||||
"",
|
||||
f"Part of [{emu_name}]({name}.md), whose page has the profile, the install "
|
||||
f"command and every entry. This part holds {_count(len(files), 'entry', 'entries')}, "
|
||||
# The crumbs end on the emulator, read from parent: (round 5, 7.5).
|
||||
f"This part holds {_count(len(files), 'entry', 'entries')}, "
|
||||
f"{_code(first)} to {_code(last)}. The other parts: {others}.",
|
||||
"",
|
||||
*_sectioned(files, list(range(len(files))),
|
||||
|
||||
@@ -11,6 +11,8 @@ import unittest
|
||||
from unittest import mock
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
|
||||
@@ -896,7 +898,8 @@ class AnEmulatorPageOffersItsInstall(unittest.TestCase):
|
||||
def test_the_command_names_the_emulator(self):
|
||||
from generate_site import _install_block
|
||||
|
||||
text = "\n".join(_install_block("pcem", {"type": "libretro"}))
|
||||
profile = yaml.safe_load((ROOT / "emulators" / "pcem.yml").read_text(encoding="utf-8"))
|
||||
text = "\n".join(_install_block("pcem", profile))
|
||||
self.assertIn("install.sh | sh -s -- --emulator pcem", text)
|
||||
self.assertIn("install.ps1))) --emulator pcem", text)
|
||||
self.assertEqual(_install_block("x", {"type": "alias"}), [])
|
||||
|
||||
Reference in new issue
Block a user