mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-27 06:07:24 -05:00
chore: merge round 5 system page fixes
This commit is contained in:
commit
4db718bb5c
9 files changed
+818
-72
No files matched your search
+378
-48
@@ -4642,14 +4642,92 @@ def _group_by_manufacturer(db: dict) -> dict[str, dict[str, list]]:
|
||||
The database keeps one path per content, and a copy filed elsewhere was
|
||||
on no page of its own: the Channel F BIOS sat under Arcade > FBNeo only,
|
||||
and Fairchild had no system page. Every folder holding a copy lists it,
|
||||
read from the full paths by_path_suffix records.
|
||||
read from the full paths by_path_suffix records. The game engines'
|
||||
files are grouped by engine (_engine_sections).
|
||||
"""
|
||||
files = db.get("files", {})
|
||||
manufacturers: dict[str, dict[str, list]] = {}
|
||||
for sha1, where in _copy_places(db).items():
|
||||
for manufacturer, console in sorted({(m, c) for m, c, _ in where}):
|
||||
manufacturers.setdefault(manufacturer, {}).setdefault(console, []).append(files[sha1])
|
||||
return manufacturers
|
||||
return _engine_sections(manufacturers)
|
||||
|
||||
|
||||
def _engine_sections(manufacturers: dict[str, dict[str, list]]) -> dict[str, dict[str, list]]:
|
||||
"""A game engine's files in one section headed with the engine's name.
|
||||
|
||||
The Systems index listed ".variants", "scummvm.zip" and "wildmidi.cfg"
|
||||
as games, and ScummVM's files sat on three pages. A folder of variants
|
||||
joins the section of its primary, a file kept at a top's root joins
|
||||
the section of the one engine that reads it (else of the engine the
|
||||
top is named after), the folder under Other named after a game-engine
|
||||
top joins that top, and a folder named by a profile key is headed with
|
||||
the engine's name (SECTION_LABELS), its address unchanged.
|
||||
"""
|
||||
from siterender import _PROGRAM_TOPS, SECTION_LABELS, record_card_move # noqa: PLC0415
|
||||
|
||||
profiles = {key: profile for key, profile in _profiles_on_disk().items()
|
||||
if profile.get("type") not in ("alias", "test")}
|
||||
readers: dict[str, set[str]] = {}
|
||||
for key, profile in profiles.items():
|
||||
for entry in profile.get("files") or []:
|
||||
if isinstance(entry, dict):
|
||||
for name in [entry.get("name"), *(entry.get("aliases") or [])]:
|
||||
if name:
|
||||
readers.setdefault(str(name).rsplit("/", 1)[-1], set()).add(key)
|
||||
|
||||
def engine(key: str) -> str:
|
||||
return str(profiles[key].get("emulator") or key)
|
||||
|
||||
def profile_of(name: str) -> str | None:
|
||||
return next((k for k in sorted(profiles)
|
||||
if name.casefold() in (k.casefold(), engine(k).casefold())), None)
|
||||
|
||||
out: dict[str, dict[str, list]] = {}
|
||||
for mfr, consoles in manufacturers.items():
|
||||
if mfr not in GAME_DATA_TOPS:
|
||||
out[mfr] = dict(consoles)
|
||||
continue
|
||||
own = profile_of(mfr)
|
||||
regrouped: dict[str, list] = {}
|
||||
for console, files in sorted(consoles.items(), key=lambda item: item[0].startswith(".")):
|
||||
for f in files:
|
||||
parts = str(f.get("path", "")).split("/")
|
||||
target = console
|
||||
if console.startswith(".") or (len(parts) == 3 and console == parts[-1]):
|
||||
# A variant answers its primary's name: scummvm.zip.a93f1c4b.
|
||||
base = parts[-1].rsplit(".", 1)[0] if console.startswith(".") else parts[-1]
|
||||
keys = sorted(readers.get(base, ()))
|
||||
key = keys[0] if len(keys) == 1 else own
|
||||
found = next((c for c, held in regrouped.items()
|
||||
if any(h.get("name") == base for h in held)), None)
|
||||
target = found or (next((c for c in consoles if c == key), None)
|
||||
or (engine(key) if key else base))
|
||||
if target != console:
|
||||
record_card_move(mfr, console, str(f.get("sha1", "")), mfr, target)
|
||||
regrouped.setdefault(target, []).append(f)
|
||||
out[mfr] = regrouped
|
||||
# The folder under Other an engine top is named after: ScummVM's
|
||||
# engine data under Other/ScummVM, its themes under ScummVM/.
|
||||
others = out.get("Other", {})
|
||||
for folder in sorted(others):
|
||||
top = next((t for t in out if t in GAME_DATA_TOPS and t.casefold() == folder.casefold()),
|
||||
None)
|
||||
if top is None or top in _PROGRAM_TOPS:
|
||||
continue
|
||||
sections = out[top]
|
||||
target = next((c for c in sections if c.casefold() == folder.casefold()), folder)
|
||||
held = {str(f.get("sha1", "")) for f in sections.get(target, [])}
|
||||
for f in others[folder]:
|
||||
record_card_move("Other", folder, str(f.get("sha1", "")), top, target)
|
||||
sections[target] = [*sections.get(target, []),
|
||||
*(f for f in others.pop(folder) if str(f.get("sha1", "")) not in held)]
|
||||
for mfr in GAME_DATA_TOPS:
|
||||
for console in out.get(mfr, {}):
|
||||
key = profiles.get(console) and console
|
||||
if key and engine(key) != console:
|
||||
SECTION_LABELS[(mfr, console)] = engine(key)
|
||||
return {mfr: consoles for mfr, consoles in out.items() if consoles}
|
||||
|
||||
|
||||
def _console_display(manufacturer: str, console: str) -> str:
|
||||
@@ -4668,6 +4746,8 @@ def generate_systems_index(manufacturers: dict) -> str:
|
||||
PlayStation had to know it is filed under Sony, and Other hid 91
|
||||
sections. Manufacturer pages stay the containers.
|
||||
"""
|
||||
for old, new in SYSTEM_PAGES_MOVED.items():
|
||||
sitestarlight.MOVED_PAGES.setdefault(old, new)
|
||||
total_mfr = len(manufacturers)
|
||||
total_consoles = sum(len(c) for c in manufacturers.values())
|
||||
# A copy filed under two folders appears on both pages, counted once here.
|
||||
@@ -4710,6 +4790,7 @@ def generate_systems_index(manufacturers: dict) -> str:
|
||||
rows = sorted(
|
||||
((_folder_emulator(console)[0] if group == "programs"
|
||||
else _section_label(mfr, console) if mfr == "Arcade"
|
||||
or (mfr, console) in SECTION_LABELS
|
||||
else _console_display(mfr, console), mfr, console, len(files))
|
||||
for mfr, consoles in manufacturers.items() if kind(mfr) == group
|
||||
for console, files in consoles.items()),
|
||||
@@ -5693,10 +5774,29 @@ def _console_cards(
|
||||
if by_name:
|
||||
return (place.get(str(f.get("sha1", "")), _NO_PRIORITY), *_by_name(f))
|
||||
return _card_rank(f, consumers, place)
|
||||
# Another revision of a MAME or FBNeo set is another release's set, its
|
||||
# Romset line saying which: it sits open beside the set of its name.
|
||||
# Twelve neogeo.zip were on five pages, the SNK ones closed under
|
||||
# Variants.
|
||||
romsets = _romset_releases()
|
||||
beside = [f for f in files if "/.variants/" in f["path"] and f["name"] in romsets]
|
||||
main_files = [f for f in files if "/.variants/" not in f["path"]]
|
||||
variant_files = [f for f in files if "/.variants/" in f["path"]]
|
||||
variant_files = [f for f in files if "/.variants/" in f["path"] and f not in beside]
|
||||
# Sets of one name side by side, the first ranked leading.
|
||||
ranked = sorted(main_files, key=order)
|
||||
by_set: dict[str, list[dict]] = {}
|
||||
for f in [*ranked, *sorted(beside, key=order)]:
|
||||
if f["name"] in romsets:
|
||||
by_set.setdefault(f["name"], []).append(f)
|
||||
ordered: list[dict] = []
|
||||
for f in ranked:
|
||||
if f["name"] not in romsets:
|
||||
ordered.append(f)
|
||||
elif by_set.get(f["name"]):
|
||||
ordered.extend(by_set.pop(f["name"]))
|
||||
ordered.extend(f for files in by_set.values() for f in files)
|
||||
cards = [(False, _system_file_card(f, consumers, anchored, searchable), str(f.get("sha1", "")))
|
||||
for f in sorted(main_files, key=order)]
|
||||
for f in ordered]
|
||||
cards += [(True, _system_file_card(v, consumers, anchored, searchable), str(v.get("sha1", "")))
|
||||
for v in sorted(variant_files, key=order)]
|
||||
return cards
|
||||
@@ -5840,11 +5940,30 @@ class _StartGroup:
|
||||
self.hle: set[str] = set()
|
||||
# Files an emulator reads when present and runs the same without.
|
||||
self.extra: list[str] = []
|
||||
# By line (a region, "" for every region): visible name -> the
|
||||
# emulators requiring that file for the line. Three Mega CD BIOS,
|
||||
# one per region, share the name cd_bios.rom.
|
||||
self.line_readers: dict[str, dict[str, set[str]]] = {}
|
||||
# (line, visible name, emulator) -> the name the emulator reads.
|
||||
self.read_as: dict[tuple[str, str, str], str] = {}
|
||||
# (line, visible name) -> the file name its card shows.
|
||||
self.card_names: dict[tuple[str, str], str] = {}
|
||||
# (line, visible name) of the files most of whose readers read
|
||||
# them beside another one they require.
|
||||
self.extras: set[tuple[str, str]] = set()
|
||||
|
||||
def require(self, name: str, emulator: str) -> None:
|
||||
self.written.setdefault(_visible(name), name)
|
||||
self.requires.setdefault(_visible(name), set()).add(emulator)
|
||||
|
||||
def read_for(self, line: str, name: str, emulator: str, read: str, card: str) -> None:
|
||||
"""Record that ``emulator`` requires ``name`` for ``line``, under
|
||||
the name ``read``, its card titled ``card``."""
|
||||
visible = _visible(name)
|
||||
self.line_readers.setdefault(line, {}).setdefault(visible, set()).add(emulator)
|
||||
self.read_as.setdefault((line, visible, emulator), read)
|
||||
self.card_names.setdefault((line, visible), card)
|
||||
|
||||
def absorb(self, other: _StartGroup) -> None:
|
||||
for region, names in other.by_region.items():
|
||||
for name in names:
|
||||
@@ -5859,10 +5978,38 @@ class _StartGroup:
|
||||
for visible, sha1 in other.sha1s.items():
|
||||
self.sha1s.setdefault(visible, sha1)
|
||||
self.hle.update(other.hle)
|
||||
for line, readers in other.line_readers.items():
|
||||
for visible, emulators in readers.items():
|
||||
self.line_readers.setdefault(line, {}).setdefault(visible, set()).update(emulators)
|
||||
for key, value in other.read_as.items():
|
||||
self.read_as.setdefault(key, value)
|
||||
for key, value in other.card_names.items():
|
||||
self.card_names.setdefault(key, value)
|
||||
self.extras |= other.extras
|
||||
|
||||
def requirers(self) -> set[str]:
|
||||
return set().union(*self.requires.values()) if self.requires else set()
|
||||
|
||||
def as_read(self, visible: str, emulators: set[str], line: str | None = None) -> str:
|
||||
"""A file as an emulator reading it instead is told to fetch it:
|
||||
by its card's name, with the name it reads when that differs.
|
||||
The line read "ares reads neo-epo.sp1" above a card titled
|
||||
neo-epo.bin."""
|
||||
lines = [line] if line is not None else sorted(self.line_readers)
|
||||
for key in lines:
|
||||
card = self.card_names.get((key, visible))
|
||||
if card is None:
|
||||
continue
|
||||
reads = {self.read_as[(key, visible, e)] for e in emulators
|
||||
if (key, visible, e) in self.read_as}
|
||||
written = self.written.get(visible, _code(card))
|
||||
# A linked name (a file under Arcade) keeps its link.
|
||||
shown = written if _visible(written) == card else _code(card)
|
||||
if len(reads) == 1 and next(iter(reads)) != card:
|
||||
return f"{shown} (as {_code(next(iter(reads)))})"
|
||||
return shown
|
||||
return self.written.get(visible, _code(visible))
|
||||
|
||||
|
||||
def _start_groups(
|
||||
files: list[dict], consumers: FileConsumers, section: tuple[str, str] | None
|
||||
@@ -5938,12 +6085,36 @@ def _start_groups(
|
||||
return _code(archive), ""
|
||||
return f"[{_code(archive)}]({markup.site_href(f'files/?sha1={found[0]}')})", ""
|
||||
|
||||
for f in ranked:
|
||||
def reading(f: dict) -> Iterator[tuple[str, dict, str | None, str, str]]:
|
||||
"""Each read of a file: the emulator, its entry, the system, the
|
||||
name the line writes and the sha1 of the card it names."""
|
||||
sha1 = str(f.get("sha1", ""))
|
||||
for emulator, entry, system in kept.get(sha1, []):
|
||||
name, named = rendered[sha1], sha1
|
||||
if entry.get("archive"):
|
||||
name, named = archive_named(str(entry["archive"]))
|
||||
yield emulator, entry, system, name, named
|
||||
|
||||
# A file as many of whose readers require another file and read this
|
||||
# one only when present is an extra, not a start file: NAOMI opened on
|
||||
# hod2bios.zip, which 13 of the 19 emulators listing it read beside
|
||||
# naomi.zip for the House of the Dead 2 alone.
|
||||
requiring: dict[str, set[str]] = {}
|
||||
optional: dict[str, set[str]] = {}
|
||||
for f in ranked:
|
||||
for emulator, entry, _system, name, named in reading(f):
|
||||
if entry.get("hle_fallback"):
|
||||
continue
|
||||
side = requiring if entry.get("required", True) else optional
|
||||
side.setdefault(named or _visible(name), set()).add(emulator)
|
||||
needs_other = {emulator: {key for key, emulators in requiring.items() if emulator in emulators}
|
||||
for emulators in requiring.values() for emulator in emulators}
|
||||
weak = {key for key, emulators in requiring.items()
|
||||
if len(emulators) <= len({e for e in optional.get(key, ())
|
||||
if needs_other.get(e, set()) - {key}})}
|
||||
|
||||
for f in ranked:
|
||||
for emulator, entry, system, name, named in reading(f):
|
||||
try:
|
||||
key = system_name(system) if system else None
|
||||
except KeyError:
|
||||
@@ -5961,9 +6132,14 @@ def _start_groups(
|
||||
_add_name(group.extra, name)
|
||||
continue
|
||||
group.require(name, emulator)
|
||||
regions = {str(r) for r in entry.get("region") or []}
|
||||
for names in [group.by_region.setdefault(r, []) for r in regions] or [group.plain]:
|
||||
_add_name(names, name)
|
||||
read = str(entry.get("archive") or entry.get("name") or _visible(name))
|
||||
card = str(entry["archive"]) if entry.get("archive") else str(f["name"])
|
||||
regions = sorted({str(r) for r in entry.get("region") or []})
|
||||
for region in regions or [""]:
|
||||
_add_name(group.by_region.setdefault(region, []) if region else group.plain, name)
|
||||
group.read_for(region, name, emulator, read, card)
|
||||
if (named or _visible(name)) in weak:
|
||||
group.extras.add((region, _visible(name)))
|
||||
named = [key for key in groups if key is not None]
|
||||
if None in groups and named:
|
||||
# An entry that names no system reads the section's own console.
|
||||
@@ -6057,6 +6233,11 @@ def _group_lines(
|
||||
if lead == "*":
|
||||
lines.append(f"- **{key}**: no file needed{text}")
|
||||
continue
|
||||
if lead == "+":
|
||||
# What the others read instead ends the machine's line.
|
||||
end = "" if lines[-1].endswith(".") else "."
|
||||
lines[-1] = f"{lines[-1]}{end} {text}"
|
||||
continue
|
||||
lines.append(f"- **{key}, {lead}**: {text}" if lead else f"- **{key}**: {text}")
|
||||
return [*lines, ""]
|
||||
|
||||
@@ -6084,11 +6265,13 @@ def _lead_file(groups: dict[str | None, _StartGroup]) -> list[str]:
|
||||
emulators: set[str] = set()
|
||||
requires: dict[str, set[str]] = {}
|
||||
written: dict[str, str] = {}
|
||||
holder: dict[str, _StartGroup] = {}
|
||||
for group in groups.values():
|
||||
emulators.update(group.emulators)
|
||||
for visible, readers in group.requires.items():
|
||||
requires.setdefault(visible, set()).update(readers)
|
||||
written.setdefault(visible, group.written[visible])
|
||||
holder.setdefault(visible, group)
|
||||
if not requires:
|
||||
return []
|
||||
top = max(requires, key=lambda v: len(requires[v]))
|
||||
@@ -6102,10 +6285,11 @@ def _lead_file(groups: dict[str | None, _StartGroup]) -> list[str]:
|
||||
lines = [f"**Start with** {written[top]}.{zipped} {said} read{'s' if len(names) == 1 else ''} it.",
|
||||
""]
|
||||
# The emulators that require other files and not this one, by the
|
||||
# files they require.
|
||||
# files they require, each by its card's name.
|
||||
others: dict[tuple[str, ...], set[str]] = {}
|
||||
for emulator in emulators - readers:
|
||||
theirs = tuple(written[v] for v in requires if emulator in requires[v])
|
||||
theirs = tuple(holder[v].as_read(v, {emulator}) for v in requires
|
||||
if emulator in requires[v])
|
||||
if theirs:
|
||||
others.setdefault(theirs, set()).add(emulator)
|
||||
sentences = []
|
||||
@@ -6146,9 +6330,48 @@ def _group_start(group: _StartGroup, first_below: bool) -> list[tuple[str, str]]
|
||||
if requirers and 2 * len(requirers) <= 2 * len(running - hle) + len(hle):
|
||||
free = _display_names(running)
|
||||
needing = _display_names(requirers)
|
||||
names = list(group.written.values())
|
||||
verb = "requires" if len(needing) == 1 else "require"
|
||||
if all(len(readers) == len(requirers) for readers in group.requires.values()):
|
||||
return [("*", f" with {_names_counted(free)}. Only {_names_counted(needing)} {verb} "
|
||||
f"{listed(names, False)}.")]
|
||||
# Each requires some of the files: "bsnes, FinalBurn Neo and 3
|
||||
# more require 22 files" read as 22 files for a bsnes user.
|
||||
these = f"these {len(names)} files" if len(names) > START_WITH_NAMES else "these files"
|
||||
return [("*", f" with {_names_counted(free)}. Only {_names_counted(needing)} {verb} "
|
||||
f"{listed(list(group.written.values()), False)}.")]
|
||||
f"some of {these}: {listed(names, False)}.")]
|
||||
|
||||
def instead_of(line: str, names: list[str], answered: set[str]) -> str:
|
||||
"""What the emulators no other name answers read instead, one
|
||||
sentence per set of files: "FinalBurn Neo reads fdsbios.zip instead"."""
|
||||
readers = group.line_readers.get(line, {})
|
||||
others: dict[tuple[str, ...], set[str]] = {}
|
||||
for emulator in sorted(set().union(*(readers.get(_visible(n), set()) for n in names))
|
||||
- answered):
|
||||
theirs = tuple(group.as_read(_visible(n), {emulator}, line) for n in names
|
||||
if emulator in readers.get(_visible(n), set()))
|
||||
others.setdefault(theirs, set()).add(emulator)
|
||||
sentences = []
|
||||
for files, who in sorted(others.items(), key=lambda item: (-len(item[1]), item[0])):
|
||||
shown = _display_names(who)
|
||||
verb = "reads" if len(shown) == 1 else "read"
|
||||
sentences.append(f"{_names_counted(shown)} {verb} {_said_as_list(list(files))} instead.")
|
||||
return " ".join(sentences)
|
||||
|
||||
def split(line: str, names: list[str]) -> tuple[list[str], str, set[str]]:
|
||||
"""A line's names as the files to start with, the sentence naming
|
||||
what other emulators read instead, and every emulator the line
|
||||
answers. Names no emulator requires beside another are other names
|
||||
for one slot: "fdsbios.zip, disksys.rom" read as two files to get."""
|
||||
readers = group.line_readers.get(line, {})
|
||||
sets = {n: readers.get(_visible(n), set()) for n in names}
|
||||
lead = max(names, key=lambda n: len(sets[n]))
|
||||
main = [n for n in names if n == lead or sets[n] <= sets[lead]]
|
||||
rest = [n for n in names if n not in main]
|
||||
# A file the lead's readers require beside it is a start file too,
|
||||
# unless most of its readers take it as an extra.
|
||||
main = [n for n in main if n == lead or (line, _visible(n)) not in group.extras]
|
||||
return main, instead_of(line, rest, set(sets[lead])), set().union(*sets.values())
|
||||
|
||||
order = _region_order()
|
||||
# A territory its region answers the same way adds nothing: Brazil
|
||||
@@ -6156,22 +6379,39 @@ def _group_start(group: _StartGroup, first_below: bool) -> list[tuple[str, str]]
|
||||
by_region = {r: names for r, names in group.by_region.items()
|
||||
if group.by_region.get(_PARENT.get(r, "")) != names}
|
||||
# Regions that need the same files share a line.
|
||||
regional: dict[tuple[str, ...], list[str]] = {}
|
||||
regional: dict[tuple[str, ...], str] = {}
|
||||
answered: set[str] = set()
|
||||
shown: set[str] = set()
|
||||
for region in sorted(by_region, key=lambda r: (order.get(r, len(order)), r)):
|
||||
names = by_region[region]
|
||||
same = next((rs for rs, held in regional.items() if held == names), None)
|
||||
main, sentence, readers = split(region, by_region[region])
|
||||
answered |= readers
|
||||
shown |= {_visible(n) for n in by_region[region]}
|
||||
text = f"{listed(main)}. {sentence}" if sentence else listed(main)
|
||||
same = next((rs for rs, held in regional.items() if held == text), None)
|
||||
if same is None:
|
||||
regional[(region,)] = names
|
||||
regional[(region,)] = text
|
||||
else:
|
||||
regional[(*same, region)] = regional.pop(same)
|
||||
|
||||
out = [(", ".join(_region_word(r) for r in regions), listed(regional[regions]))
|
||||
out = [(", ".join(_region_word(r) for r in regions), regional[regions])
|
||||
for regions in sorted(regional, key=lambda rs: (order.get(rs[0], len(order)), rs))]
|
||||
# A name a region line gives already is no "also".
|
||||
shown = {_visible(n) for names in regional.values() for n in names}
|
||||
# A name a region line gives already is no "also", and one only
|
||||
# emulators no region line answers reads is an "instead": armsx1 reads
|
||||
# scph1001.bin where the others pick a BIOS by region.
|
||||
plain = [n for n in group.plain if _visible(n) not in shown]
|
||||
if plain:
|
||||
out.append(("", listed(plain)))
|
||||
if plain and out:
|
||||
readers = group.line_readers.get("", {})
|
||||
also = [n for n in plain if readers.get(_visible(n), set()) & answered]
|
||||
if also:
|
||||
out.append(("", listed(also)))
|
||||
sentence = instead_of("", [n for n in plain if n not in also], answered)
|
||||
if sentence:
|
||||
out.append(("+", sentence))
|
||||
elif plain:
|
||||
main, sentence, _readers = split("", plain)
|
||||
out.append(("", listed(main)))
|
||||
if sentence:
|
||||
out.append(("+", sentence))
|
||||
if not out and group.optional:
|
||||
out.append(("*", f"{_without_files(group.emulators)}. "
|
||||
f"For full accuracy: {listed(group.optional)}."))
|
||||
@@ -6191,14 +6431,19 @@ def _start_lines(block: list[tuple[str, str]], console: str = "") -> list[str]:
|
||||
lead, text = block[0]
|
||||
if lead == "*":
|
||||
return [f"**No file needed**{text}", ""]
|
||||
if len(block) == 1 and lead == "":
|
||||
named, _sep, where = text.partition(", first below")
|
||||
# What the emulators no line answers read instead, after the lines.
|
||||
instead = " ".join(text for lead, text in block if lead == "+")
|
||||
block = [(lead, text) for lead, text in block if lead != "+"]
|
||||
if len(block) == 1 and block[0][0] == "":
|
||||
named, _sep, _where = block[0][1].partition(", first below")
|
||||
below = " They come first below." if _sep else ""
|
||||
return [f"**Start with**: {named}.{below}", ""]
|
||||
return [" ".join(filter(None, [f"**Start with**: {named}.{below}", instead])), ""]
|
||||
bios = f"the {console} BIOS" if console else "the BIOS"
|
||||
lines = [f"**Start with** {bios} for the region of your games:", ""]
|
||||
for lead, text in block:
|
||||
lines.append(f"- **{lead or 'Some emulators also require'}**: {text}")
|
||||
if instead:
|
||||
lines += ["", instead]
|
||||
return [*lines, ""]
|
||||
|
||||
|
||||
@@ -6262,6 +6507,23 @@ _BOARD_CONSOLES: dict[str, str] = {}
|
||||
# its maker, which said nothing about what it holds.
|
||||
OTHER_ARCADE = ("Arcade", "Arcade")
|
||||
OTHER_ARCADE_LABEL = "Other arcade files"
|
||||
# (manufacturer, console) -> the boards whose sets left that folder's
|
||||
# section for the boards' own pages. The section names them.
|
||||
_BOARDS_FROM: dict[tuple[str, str], set[str]] = {}
|
||||
# System pages published under an address no page holds since, and where
|
||||
# their files went: the Atom and the MZ were boards, ScummVM's and SDLPAL's
|
||||
# files were on two pages, the Dreamcast lost its NAOMI sets. An address
|
||||
# redirects only while no page takes it back (sitestarlight.redirects).
|
||||
SYSTEM_PAGES_MOVED = {
|
||||
"systems/arcade-acorn-atom.md": "systems/acorn.md",
|
||||
"systems/arcade-sharp-mz-2000.md": "systems/sharp.md",
|
||||
"systems/arcade-sharp-mz-2500.md": "systems/sharp.md",
|
||||
"systems/arcade-miuchiz.md": "systems/arcade.md",
|
||||
"systems/arcade-yamaha-ym2608.md": "systems/arcade.md",
|
||||
"systems/other-scummvm.md": "systems/scummvm.md",
|
||||
"systems/other-sdlpal.md": "systems/sdlpal.md",
|
||||
"systems/sega-dreamcast.md": "systems/sega.md",
|
||||
}
|
||||
|
||||
|
||||
def arcade_board_sections(
|
||||
@@ -6274,18 +6536,23 @@ def arcade_board_sections(
|
||||
ZN-1 had no section, and their names stayed plain on 568 emulator
|
||||
pages. Each archive goes to the board siterender.arcade_board() reads
|
||||
from the profiles, never from its folder, and a folder named after a
|
||||
board (Namco System 246) joins it. Files no board claims stay in their
|
||||
board (Namco System 246) joins it, whatever maker's folder holds the
|
||||
copy. A set a reviewed machine owns (siterender.archive_machine) goes
|
||||
to that machine's section. Files neither claims stay in their
|
||||
folders. Each board is recorded (siterender.record_board_section) before
|
||||
any page links one, and a system's install commands follow its link to
|
||||
the board's section. Call after reset_console_pages()."""
|
||||
from siterender import ( # noqa: PLC0415
|
||||
arcade_board,
|
||||
arcade_boards,
|
||||
archive_machine,
|
||||
record_board_section,
|
||||
record_card_move,
|
||||
system_section,
|
||||
)
|
||||
|
||||
_BOARD_CONSOLES.clear()
|
||||
_BOARDS_FROM.clear()
|
||||
arcade = manufacturers.get("Arcade")
|
||||
if not arcade:
|
||||
return manufacturers, installs
|
||||
@@ -6294,23 +6561,66 @@ def arcade_board_sections(
|
||||
boards: dict[str, list] = {}
|
||||
folders: dict[str, list] = {}
|
||||
seen: dict[str, set[str]] = {}
|
||||
# The sets a reviewed machine owns go to its section: neogeo.zip sat on
|
||||
# five Arcade pages, the Atom's and the MZ-2000's sets on board pages.
|
||||
machines: dict[tuple[str, str], list] = {}
|
||||
|
||||
def file_on_board(board: str, f: dict) -> None:
|
||||
# One card per content: a copy under Arcade and one under MAME
|
||||
# are the same file.
|
||||
if str(f.get("sha1", "")) in seen.setdefault(board, set()):
|
||||
return
|
||||
seen[board].add(str(f.get("sha1", "")))
|
||||
boards.setdefault(board, []).append(f)
|
||||
|
||||
for console in sorted(arcade):
|
||||
joined = by_name.get(console.casefold())
|
||||
for f in arcade[console]:
|
||||
board = joined or arcade_board(str(f.get("sha1", "")))
|
||||
if board is None:
|
||||
sha1 = str(f.get("sha1", ""))
|
||||
board = joined or arcade_board(sha1)
|
||||
machine = None if board else archive_machine(sha1)
|
||||
if board is not None:
|
||||
file_on_board(board, f)
|
||||
elif machine is not None:
|
||||
machines.setdefault(machine, []).append(f)
|
||||
record_card_move("Arcade", console, sha1, *machine)
|
||||
else:
|
||||
folders.setdefault(console, []).append(f)
|
||||
continue
|
||||
# One card per content: a copy under Arcade and one under MAME
|
||||
# are the same file.
|
||||
if str(f.get("sha1", "")) in seen.setdefault(board, set()):
|
||||
continue
|
||||
seen[board].add(str(f.get("sha1", "")))
|
||||
boards.setdefault(board, []).append(f)
|
||||
# A board's set filed under a machine's folder is on the board's page
|
||||
# too, whatever the folder: naomi.zip under the Dreamcast was the copy
|
||||
# five setups list, and the NAOMI page did not name it.
|
||||
makers: dict[str, dict[str, list]] = {}
|
||||
for mfr, consoles in manufacturers.items():
|
||||
if mfr == "Arcade":
|
||||
continue
|
||||
for console, files in consoles.items():
|
||||
kept = []
|
||||
for f in files:
|
||||
sha1 = str(f.get("sha1", ""))
|
||||
board = arcade_board(sha1)
|
||||
machine = None if board else archive_machine(sha1)
|
||||
if board is not None:
|
||||
file_on_board(board, f)
|
||||
_BOARDS_FROM.setdefault((mfr, console), set()).add(board)
|
||||
elif machine is not None and machine != (mfr, console):
|
||||
# The Atom's own set sat with the BBC Micro.
|
||||
machines.setdefault(machine, []).append(f)
|
||||
record_card_move(mfr, console, sha1, *machine)
|
||||
else:
|
||||
kept.append(f)
|
||||
if len(kept) != len(files):
|
||||
makers.setdefault(mfr, dict(consoles))[console] = kept
|
||||
for (mfr, console), files in sorted(machines.items()):
|
||||
consoles = makers.setdefault(mfr, dict(manufacturers.get(mfr, {})))
|
||||
held = {str(f.get("sha1", "")) for f in consoles.get(console, [])}
|
||||
consoles[console] = [*consoles.get(console, []),
|
||||
*(f for f in files if str(f.get("sha1", "")) not in held)]
|
||||
for board in sorted(boards):
|
||||
record_board_section(board)
|
||||
_BOARD_CONSOLES[names[board]] = board
|
||||
regrouped = {**folders, **{names[board]: files for board, files in boards.items()}}
|
||||
makers = {mfr: {console: files for console, files in consoles.items() if files}
|
||||
for mfr, consoles in makers.items()}
|
||||
|
||||
moved: dict[str, dict[str, list[tuple[str, str]]]] = {}
|
||||
for slug, consoles in installs.items():
|
||||
@@ -6322,7 +6632,21 @@ def arcade_board_sections(
|
||||
else:
|
||||
slug_to, console_to = slug, console
|
||||
moved.setdefault(slug_to, {}).setdefault(console_to, []).append((platform, sys_id))
|
||||
return {**manufacturers, "Arcade": regrouped}, moved
|
||||
return {**manufacturers, **makers, "Arcade": regrouped}, moved
|
||||
|
||||
|
||||
def _boards_line(manufacturer: str, console: str) -> list[str]:
|
||||
"""The line of a section whose board sets went to the boards' pages:
|
||||
the Dreamcast folder holds NAOMI's and Atomiswave's."""
|
||||
from siterender import arcade_boards # noqa: PLC0415
|
||||
|
||||
boards = _BOARDS_FROM.get((manufacturer, console))
|
||||
if not boards:
|
||||
return []
|
||||
names = arcade_boards()
|
||||
links = [f"[{names[board]}]({console_href('arcade', names[board]).removeprefix('systems/')})"
|
||||
for board in sorted(boards, key=lambda b: names[b].casefold())]
|
||||
return [f"**Arcade board BIOS** on the boards' own pages: {', '.join(links)}.", ""]
|
||||
|
||||
|
||||
def _section_label(manufacturer: str, console: str) -> str:
|
||||
@@ -6433,7 +6757,8 @@ def system_pages(
|
||||
def start_of(console: str, first_below: bool = True) -> list[str]:
|
||||
if boards and console not in boards:
|
||||
return []
|
||||
return _start_with(consoles[console], consumers, first_below, (manufacturer, console))
|
||||
return [*_start_with(consoles[console], consumers, first_below, (manufacturer, console)),
|
||||
*_boards_line(manufacturer, console)]
|
||||
|
||||
starts = {console: [] if boards and console not in boards
|
||||
else _start_order(consoles[console], consumers, (manufacturer, console))
|
||||
@@ -6592,9 +6917,12 @@ def system_pages(
|
||||
])
|
||||
lead = set(starts[console])
|
||||
# A part by the files it holds: "Arcade: Arcade, part 3 of 5" said
|
||||
# nothing a search result could use.
|
||||
titles = ([f"{manufacturer}: {label}"] if len(stems) == 1 else [
|
||||
"{} files {} to {}".format(_part_name(manufacturer, console), *_part_ends(part, lead))
|
||||
# nothing a search result could use. A folder under Other goes by
|
||||
# its emulator's name: "mame files" named the folder.
|
||||
named = _folder_emulator(console)[0] if manufacturer == "Other" else None
|
||||
titles = ([f"{manufacturer}: {named or label}"] if len(stems) == 1 else [
|
||||
"{} files {} to {}".format(named or _part_name(manufacturer, console),
|
||||
*_part_ends(part, lead))
|
||||
for part in parts])
|
||||
count = _count(len(consoles[console]), "file")
|
||||
summary.extend(start_of(console, False))
|
||||
@@ -6617,25 +6945,23 @@ def system_pages(
|
||||
following=neighbours[-1] if index + 1 < len(stems) else None,
|
||||
weight=_part_weight() if len(stems) > 1 else None,
|
||||
consoles=[label] if index == 0 and console in machines else None),
|
||||
f"# {titles[index]}", "",
|
||||
f"Part of [{manufacturer}]({slug}.md), in [Systems](index.md).",
|
||||
"", *_icon_symbol(),
|
||||
# The crumbs end on the maker's page: no line says it again.
|
||||
f"# {titles[index]}", "", *_icon_symbol(),
|
||||
]
|
||||
if len(stems) > 1:
|
||||
others = ", ".join(
|
||||
f"[part {i + 1}]({other}.md)" for i, other in enumerate(stems) if i != index)
|
||||
lines.extend([f"This part holds {_part_range(part, lead)}. The others: {others}.",
|
||||
""])
|
||||
if board:
|
||||
# The heading a board's links land on, above its start line.
|
||||
lines.extend([f"## {label} {{ #{board_slug(board)} }}", ""])
|
||||
# A board's page is its one section: its title names it, and a
|
||||
# heading of the same name said it a third time. Its cards come
|
||||
# under "Files", as every page of one console's.
|
||||
if index == 0:
|
||||
lines.extend(start_of(console, len(stems) == 1))
|
||||
lines.extend(legend)
|
||||
lines.extend(_romset_note({console: consoles[console]}))
|
||||
lines.extend(install_block(console))
|
||||
if not board:
|
||||
lines.extend(["## Files", ""])
|
||||
lines.extend(["## Files", ""])
|
||||
lines.extend(_cards_markdown(part))
|
||||
lines.append("")
|
||||
lines.extend(_next_block([f"[{manufacturer}]({slug}.md), every section"]
|
||||
@@ -7487,8 +7813,12 @@ def _console_install_block(console: str, declared: list[tuple[str, str]]) -> lis
|
||||
ps = [line for platform, system in declared for line in (
|
||||
f"# {_platform_display(platform)}",
|
||||
install_command("powershell", f"--platform {platform} --system {_ps_quote(system)}"))]
|
||||
# The name last: "Install the Atari System 1 files" read as a count.
|
||||
return search_excluded(markup.details("tip", f"Install the files of {console}", [
|
||||
# The name last: "Install the Atari System 1 files" read as a count. A
|
||||
# label naming files already reads "Install these files": "Install
|
||||
# the files of Other arcade files".
|
||||
summary = ("Install these files" if console.casefold().endswith(" files")
|
||||
else f"Install the files of {console}")
|
||||
return search_excluded(markup.details("tip", summary, [
|
||||
"Each platform names the system its own way.",
|
||||
"",
|
||||
"Linux, macOS, Steam Deck:",
|
||||
|
||||
+87
-10
@@ -193,28 +193,44 @@ def console_anchor(manufacturer: str, console: str) -> str:
|
||||
def console_href(slug: str, console: str, prefix: str = "") -> str:
|
||||
"""Where a console's files are: its own page, or its section of the
|
||||
manufacturer's page (also where a console cut into parts lists them).
|
||||
An arcade board's section heads the page of its own name."""
|
||||
An arcade board's section is the page of its own name, no heading
|
||||
repeating its title."""
|
||||
if slug == "arcade" and _board_sections:
|
||||
board = _board_ids_by_name().get(console.casefold())
|
||||
if board in _board_sections:
|
||||
anchor = board_slug(board)
|
||||
return f"{prefix}systems/arcade-{urllib.parse.quote(anchor)}.md#{anchor}"
|
||||
return f"{prefix}systems/arcade-{urllib.parse.quote(board_slug(board))}.md"
|
||||
stems = _console_pages.get((slug, console))
|
||||
if stems and len(stems) == 1:
|
||||
return f"{prefix}systems/{urllib.parse.quote(stems[0])}.md"
|
||||
return f"{prefix}systems/{urllib.parse.quote(slug)}.md#{console_anchor(slug, console)}"
|
||||
|
||||
|
||||
# (manufacturer slug, folder, sha1) -> the (manufacturer slug, console) a
|
||||
# copy's card went to when the system pages file it outside its folder:
|
||||
# ScummVM's engine data under Other/ScummVM, neogeo.zip under Arcade.
|
||||
_card_moves: dict[tuple[str, str, str], tuple[str, str]] = {}
|
||||
|
||||
|
||||
def record_card_move(manufacturer: str, folder: str, sha1: str,
|
||||
to_manufacturer: str, to_console: str) -> None:
|
||||
"""Mark that the card of the copy under bios/<manufacturer>/<folder>/
|
||||
sits in the section (to_manufacturer, to_console)."""
|
||||
_card_moves[(_manufacturer_slug(manufacturer), folder, str(sha1))] = (
|
||||
_manufacturer_slug(to_manufacturer), to_console)
|
||||
|
||||
|
||||
def card_page(path: str, sha1: str) -> str:
|
||||
"""The systems/ page stem holding the card of the copy at ``path``
|
||||
(bios/<Manufacturer>/<Console>/...)."""
|
||||
(bios/<Manufacturer>/<Console>/...). A board's set is on the board's
|
||||
page whatever folder holds the copy."""
|
||||
parts = path.split("/")
|
||||
slug = parts[1].lower().replace(" ", "-")
|
||||
console = parts[2] if len(parts) > 2 else ""
|
||||
if slug == "arcade" and _board_sections:
|
||||
if _board_sections:
|
||||
board = arcade_board(sha1)
|
||||
if board in _board_sections:
|
||||
return f"arcade-{board_slug(board)}"
|
||||
slug, console = _card_moves.get((slug, console, str(sha1)), (slug, console))
|
||||
return _card_pages.get((slug, console, sha1), slug)
|
||||
|
||||
|
||||
@@ -246,6 +262,10 @@ def system_section(sys_id: str) -> tuple[str, str] | None:
|
||||
|
||||
A heading under Arcade counts when it reads the name exactly (Namco
|
||||
System 246). One under Other never does, its headings name programs."""
|
||||
if sys_id in BOARD_PARENTS and _board_sections:
|
||||
board = _board_of_id(BOARD_PARENTS[sys_id])
|
||||
if board in _board_sections:
|
||||
return "arcade", system_name(board)
|
||||
section = _section_without_boards(sys_id)
|
||||
if (not _board_sections or sys_id in SYSTEM_PARENTS
|
||||
or (section is not None and section[0] != "arcade")):
|
||||
@@ -301,6 +321,9 @@ _arcade_source: list[tuple[dict, dict] | None] = [None]
|
||||
_archive_boards: dict[str, str] = {}
|
||||
# Casefolded board name -> the board id that stands for it.
|
||||
_board_names: dict[str, str] = {}
|
||||
# sha1 -> the (manufacturer, console) sections of the machines whose own
|
||||
# set the archive is: neogeo.zip for the Neo Geo, mz2000.zip for the MZ.
|
||||
_archive_machines: dict[str, set[tuple[str, str]]] = {}
|
||||
_boards_built = [False]
|
||||
|
||||
|
||||
@@ -392,6 +415,7 @@ def _build_boards() -> None:
|
||||
|
||||
# sha1 -> board name -> the profiles naming it. None: a machine's own.
|
||||
votes: dict[str, dict[str, set[str]] | None] = {}
|
||||
_archive_machines.clear()
|
||||
ids_by_name: dict[str, set[str]] = {}
|
||||
for key in sorted(profiles):
|
||||
profile = profiles[key]
|
||||
@@ -426,8 +450,18 @@ def _build_boards() -> None:
|
||||
for owner in owners:
|
||||
role = _board_role(owner)
|
||||
if role == _MACHINE:
|
||||
# The profile's only system stands for an entry that names
|
||||
# none: MAME's printer and expansion sets in a Dragon
|
||||
# profile. Only an entry naming the machine moves its set,
|
||||
# and only when every machine it names has that section:
|
||||
# epson_fx80.zip, which sixteen machines load, has none.
|
||||
declared = entry.get("system") or entry.get("systems")
|
||||
places = {_machine_section(o) for o in owners
|
||||
if _board_role(o) == _MACHINE} if declared else {None}
|
||||
section = next(iter(places)) if len(places) == 1 else None
|
||||
for sha1 in archives:
|
||||
votes[sha1] = None
|
||||
_archive_machines.setdefault(sha1, set()).add(section)
|
||||
continue
|
||||
if role is None:
|
||||
continue
|
||||
@@ -458,6 +492,27 @@ def _build_boards() -> None:
|
||||
_boards_built[0] = True
|
||||
|
||||
|
||||
def _machine_section(sys_id: str) -> tuple[str, str] | None:
|
||||
"""The (manufacturer, console) section SYSTEM_PARENTS files a machine
|
||||
id under, outside Arcade. Only a reviewed parent moves an archive out
|
||||
of the folder holding it: an id with a section of its own keeps its
|
||||
archives where the collection files them."""
|
||||
parent = SYSTEM_PARENTS.get(sys_id)
|
||||
if parent is None or parent[0] == "Arcade" or parent not in _section_pairs:
|
||||
return None
|
||||
return parent
|
||||
|
||||
|
||||
def archive_machine(sha1: str) -> tuple[str, str] | None:
|
||||
"""The (manufacturer, console) section of the machine an archive is
|
||||
the set of, when a profile declares it for one machine only: the Neo
|
||||
Geo's neogeo.zip, the MZ-2000's mz2000.zip. Read from the same
|
||||
``category: bios_zip`` entries as arcade_board(), never the folder."""
|
||||
_build_boards()
|
||||
places = _archive_machines.get(str(sha1), set())
|
||||
return next(iter(places)) if len(places) == 1 else None
|
||||
|
||||
|
||||
def _board_ids_by_name() -> dict[str, str]:
|
||||
_build_boards()
|
||||
return _board_names
|
||||
@@ -674,6 +729,12 @@ def set_console_sections(
|
||||
{(mfr, console) for mfr, consoles in sections.items() for console in consoles}
|
||||
if isinstance(sections, dict) else set(sections)
|
||||
)
|
||||
# A reviewed parent under a maker the pages carry is a section even with
|
||||
# no folder of its own: the Atom's sets sit under bios/Arcade, and the
|
||||
# system pages file them under Acorn (generate_site.arcade_board_sections).
|
||||
makers = {mfr for mfr, _console in pairs}
|
||||
pairs |= {parent for parent in SYSTEM_PARENTS.values()
|
||||
if parent[0] in makers and parent[0] not in _PROGRAM_TOPS}
|
||||
from sitestarlight import _unique, toc_slug
|
||||
|
||||
_console_sections.clear()
|
||||
@@ -759,7 +820,7 @@ def _heading_name(mfr: str, console: str) -> str:
|
||||
from common import GAME_DATA_TOPS
|
||||
|
||||
if mfr in GAME_DATA_TOPS:
|
||||
return console
|
||||
return SECTION_LABELS.get((mfr, console), console)
|
||||
if console.casefold().startswith(mfr.split()[0].casefold()):
|
||||
return console
|
||||
return f"{mfr} {console}"
|
||||
@@ -816,7 +877,6 @@ SYSTEM_NAMES: dict[str, str] = {
|
||||
"atari-tt": "Atari TT",
|
||||
"atari-zn1": "Atari ZN-1",
|
||||
"atlus-zn1": "Atlus ZN-1",
|
||||
"atom": "Acorn Atom",
|
||||
"bandai-playdia": "Bandai Playdia",
|
||||
"bandai-wswan": "Bandai WonderSwan",
|
||||
"bandai-wswanc": "Bandai WonderSwan Color",
|
||||
@@ -914,7 +974,6 @@ SYSTEM_NAMES: dict[str, str] = {
|
||||
"gameplan-mpu1": "Game Plan MPU-1",
|
||||
"gamepock": "Epoch Game Pocket Computer",
|
||||
"gametank": "GameTank console",
|
||||
"gba": "Nintendo Game Boy Advance",
|
||||
"gearbox": "Half-Life: Opposing Force",
|
||||
"ghostship": "Super Mario 64",
|
||||
"glulx": "Glulx",
|
||||
@@ -1281,6 +1340,12 @@ SYSTEM_PARENTS: dict[str, tuple[str, str]] = {
|
||||
"philips-vg5k": ("Philips", "VG5000"),
|
||||
"vg5k": ("Philips", "VG5000"),
|
||||
"videobrain": ("VideoBrain", "Family Computer"),
|
||||
# Computers whose sets sit under bios/Arcade with MAME's: the Atom has
|
||||
# no folder of its own, its section holds those sets. The MZ-2000 and
|
||||
# MZ-2500 sets join the MZ ones.
|
||||
"atom": ("Acorn", "Atom"),
|
||||
"mz2000": ("Sharp", "MZ"),
|
||||
"mz2500": ("Sharp", "MZ"),
|
||||
# Files kept at the maker's root, so each file is a heading of its own
|
||||
# on a page that is the machine's: the first one leads there.
|
||||
"galaksija": ("Galaksija", "Galaksija"),
|
||||
@@ -1307,7 +1372,7 @@ SYSTEM_PARENTS: dict[str, tuple[str, str]] = {
|
||||
"bbc-master-et": ("Other", "beebem"),
|
||||
"cave-story": ("Other", "NXEngine"),
|
||||
"dos": ("DOS", "DOSBox"),
|
||||
"scummvm": ("Other", "ScummVM"),
|
||||
"scummvm": ("ScummVM", "ScummVM"),
|
||||
"sinclair-zx-spectrum-next": ("Other", "cspect"),
|
||||
"solarus": ("Game Engines", "Solarus"),
|
||||
"sunplus-spmp8000": ("Other", "spmp8000emu"),
|
||||
@@ -1317,8 +1382,20 @@ SYSTEM_PARENTS: dict[str, tuple[str, str]] = {
|
||||
"xrick": ("Other", "xrick"),
|
||||
"zc210": ("ZC", "ZC210"),
|
||||
# The collection's general arcade section, where naomi.zip, neogeo.zip
|
||||
# and the other board sets sit.
|
||||
# and the other board sets sit. A sound chip's set and a toy's stay in
|
||||
# the Arcade folders holding them: neither is an arcade board.
|
||||
"arcade": ("Arcade", "Arcade"),
|
||||
"miuchiz": ("Arcade", "MAME"),
|
||||
"ym2608": ("Arcade", "Arcade"),
|
||||
}
|
||||
|
||||
|
||||
# A system id that names a family of arcade boards -> the board its link
|
||||
# leads to. RetroArch's sega-dreamcast-arcade lists the NAOMI, NAOMI 2 and
|
||||
# Atomiswave BIOS: the NAOMI page comes first, the Dreamcast section links
|
||||
# the others.
|
||||
BOARD_PARENTS: dict[str, str] = {
|
||||
"sega-dreamcast-arcade": "sega-naomi",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -361,7 +361,8 @@ class AVariantShowsWhoPinsIt(unittest.TestCase):
|
||||
platforms={"c" * 40: {"batocera", "retrobat"}}, emulators={"a" * 40: {"mame"}})
|
||||
page = generate_site.generate_system_page(
|
||||
"Entex", {"Adventure Vision": [primary, variant]}, consumers)
|
||||
block = page[page.index("<summary>Variants (1)</summary>"):]
|
||||
# Another revision of a set sits open beside it (round 5, 6.3).
|
||||
block = page[page.index('id="sha1-' + "c" * 12 + '"'):]
|
||||
self.assertIn("<dt>Collection path</dt><dd><code>bios/Entex/Adventure Vision/"
|
||||
".variants/advision.zip.1c086064</code>", block)
|
||||
self.assertIn('<a href="../platforms/batocera.md">Batocera</a>', block)
|
||||
|
||||
@@ -114,7 +114,11 @@ class TheConsoleBiosComesFirst(unittest.TestCase):
|
||||
|
||||
def test_a_long_list_names_the_first_and_counts_the_rest(self):
|
||||
files = [_file(f"rom{i}.bin", "abcdefgh"[i]) for i in range(8)]
|
||||
# One emulator requires all eight: files each required by another
|
||||
# emulator are other names for one slot (round 5, 6.2).
|
||||
consumers = _consumers({f["sha1"]: [{"required": True}] for f in files})
|
||||
consumers.emulator_files = {"emu": [{"required": True}] * len(files)}
|
||||
consumers.emulator_entries = {f["sha1"]: {"emu": i} for i, f in enumerate(files)}
|
||||
self.assertEqual(gs._start_with(files, consumers)[0],
|
||||
"**Start with**: `rom0.bin`, `rom1.bin`, `rom2.bin` and 5 more files. "
|
||||
"They come first below.")
|
||||
|
||||
@@ -84,10 +84,36 @@ class MetaFields(unittest.TestCase):
|
||||
match = re.search(r'data-pagefind-meta="aliases:([^"]*)"', text)
|
||||
self.assertIsNotNone(match, "no aliases field on the PlayStation page")
|
||||
self.assertIn("psx", unescape(match[1]).split())
|
||||
dreamcast = (DOCS / "systems" / "sega-dreamcast.md").read_text(encoding="utf-8")
|
||||
# The Dreamcast section's page: its own, or Sega's when it fits there.
|
||||
own = DOCS / "systems" / "sega-dreamcast.md"
|
||||
dreamcast = (own if own.is_file() else DOCS / "systems" / "sega.md").read_text(
|
||||
encoding="utf-8")
|
||||
self.assertRegex(dreamcast, r'data-pagefind-meta="aliases:[^"]*\bdc\b')
|
||||
|
||||
|
||||
class SystemPartsSitUnderTheirMaker(unittest.TestCase):
|
||||
"""The crumbs say where a system part sits, read from its parent: field.
|
||||
The "Part of" line under them said it again (round 5, 6.9)."""
|
||||
|
||||
def test_every_part_names_its_maker_as_parent(self) -> None:
|
||||
pages = sorted((DOCS / "systems").glob("*.md"))
|
||||
if not pages:
|
||||
self.skipTest("site pages not generated")
|
||||
makers = {page.stem for page in pages}
|
||||
orphans = []
|
||||
for page in pages:
|
||||
text = page.read_text(encoding="utf-8")
|
||||
front = text.split("\n---\n", 1)[0]
|
||||
parent = re.search(r"^parent:\n label: .+\n link: /retrobios/systems/([a-z0-9-]+)/$",
|
||||
front, re.M)
|
||||
if parent is None:
|
||||
continue
|
||||
if parent[1] not in makers:
|
||||
orphans.append(page.name)
|
||||
self.assertNotIn("\nPart of [", text, page.name)
|
||||
self.assertEqual(orphans, [])
|
||||
|
||||
|
||||
class MovedPlatformPages(unittest.TestCase):
|
||||
def test_a_setups_list_for_one_console_is_a_reference(self) -> None:
|
||||
source = Path(generate_site.__file__).read_text(encoding="utf-8")
|
||||
|
||||
@@ -42,7 +42,7 @@ UNLINKED: dict[str, str] = {
|
||||
"namco-nb1": "Arcade > FBNeo",
|
||||
"omicron": "Other > omicron",
|
||||
"rpgmaker": "RPG Maker > easyrpg",
|
||||
"sdlpal": "Other > sdlpal",
|
||||
"sdlpal": "the SDLPAL section under its own top, named after the program",
|
||||
"sega-system16b": "Arcade > FBNeo, and isgsm.zip is also the ISG Selection Master's",
|
||||
"smbx": "Game Engines > TheXTech",
|
||||
"thomson-mo6": "Other > Clock Signal",
|
||||
@@ -50,7 +50,6 @@ UNLINKED: dict[str, str] = {
|
||||
# Files filed with another machine: a link would name the wrong one.
|
||||
"commodore-c64-supercpu": "VICE's tree under the C128",
|
||||
"emerson-arcadia-2001": "ar_bios.zip is filed with the Saturn",
|
||||
"sega-dreamcast-arcade": "NAOMI, NAOMI 2 and Atomiswave sets, three boards",
|
||||
"spectravideo": "the SVI ROMs are filed with the MSX",
|
||||
"taito-type-x": "mame2010 files taitogn.zip, the G-NET BIOS, under this id",
|
||||
"vvvvvv": "SDL's controller database",
|
||||
@@ -105,7 +104,7 @@ class SystemLinks(unittest.TestCase):
|
||||
load_platform_config,
|
||||
resolve_local_file,
|
||||
)
|
||||
from generate_site import _copy_places, _group_by_manufacturer
|
||||
from generate_site import _group_by_manufacturer, arcade_board_sections
|
||||
|
||||
with open("database.json", encoding="utf-8") as handle:
|
||||
db = json.load(handle)
|
||||
@@ -117,8 +116,9 @@ class SystemLinks(unittest.TestCase):
|
||||
# (round 4, 6.4) and record it. The built pages are checked by
|
||||
# the round 4 gate.
|
||||
siterender.reset_console_pages()
|
||||
for board in siterender.arcade_boards():
|
||||
siterender.record_board_section(board)
|
||||
# The sections as the system pages hold them: each board, the
|
||||
# sets a reviewed machine owns under that machine.
|
||||
cls.sections, _installs = arcade_board_sections(cls.manufacturers, {})
|
||||
platforms = [
|
||||
load_platform_config(name, "platforms")
|
||||
for name in list_registered_platforms("platforms", include_archived=True)
|
||||
@@ -141,7 +141,12 @@ class SystemLinks(unittest.TestCase):
|
||||
for owner in owners:
|
||||
entries.setdefault(owner, []).append(fe)
|
||||
|
||||
places = _copy_places(db)
|
||||
# Where each content has a card.
|
||||
places: dict[str, set[tuple[str, str, str]]] = {}
|
||||
for mfr, consoles in cls.sections.items():
|
||||
for console, files in consoles.items():
|
||||
for f in files:
|
||||
places.setdefault(f["sha1"], set()).add((mfr, console, f["name"]))
|
||||
by_path = {entry.get("path"): sha1 for sha1, entry in db["files"].items()}
|
||||
# Id -> the (manufacturer, console) sections holding its files.
|
||||
cls.held: dict[str, set[tuple[str, str]]] = {}
|
||||
@@ -210,7 +215,7 @@ class SystemLinks(unittest.TestCase):
|
||||
def test_parents_are_sections(self) -> None:
|
||||
sections = {
|
||||
(mfr, console)
|
||||
for mfr, consoles in self.manufacturers.items() for console in consoles
|
||||
for mfr, consoles in self.sections.items() for console in consoles
|
||||
}
|
||||
self.assertEqual(
|
||||
sorted(k for k, v in siterender.SYSTEM_PARENTS.items() if v not in sections), [])
|
||||
|
||||
@@ -59,7 +59,6 @@ STAYS_PLAIN = {
|
||||
"fpinball": "scripten.exe, filed with Windows",
|
||||
"pv1000": "only MAME's software list pv1000.xml",
|
||||
"vc4000": "only MAME's software list vc4000.xml",
|
||||
"sega-dreamcast-arcade": "NAOMI, NAOMI 2 and Atomiswave sets, three boards",
|
||||
"spectravideo": "the SVI ROMs are filed with the MSX",
|
||||
"vgmplay": "sound chip sets, no board",
|
||||
"vvvvvv": "SDL's controller database",
|
||||
@@ -139,9 +138,11 @@ class Boards(unittest.TestCase):
|
||||
slug = siterender.board_slug(board)
|
||||
self.assertTrue(slug)
|
||||
self.assertEqual(slug, toc_slug(name))
|
||||
# The board's page is its section: no heading repeats its
|
||||
# title (round 5, 6.8).
|
||||
self.assertEqual(
|
||||
siterender.console_href(*siterender.system_section(board)),
|
||||
f"systems/arcade-{slug}.md#{slug}")
|
||||
f"systems/arcade-{slug}.md")
|
||||
|
||||
def test_aliases_share_a_board(self) -> None:
|
||||
"""Every id of one board's name leads to one section."""
|
||||
@@ -171,7 +172,7 @@ class Boards(unittest.TestCase):
|
||||
def test_parents_named_by_the_plan(self) -> None:
|
||||
expect = {
|
||||
"dos": ("dos", "DOSBox"),
|
||||
"scummvm": ("other", "ScummVM"),
|
||||
"scummvm": ("scummvm", "ScummVM"),
|
||||
"tyrian": ("other", "opentyrian"),
|
||||
"bbc-master-128": ("other", "beebem"),
|
||||
}
|
||||
|
||||
@@ -161,10 +161,12 @@ class EachArcadeBoardHasASection(unittest.TestCase):
|
||||
"""6.4: the Arcade page was cut by storage folder."""
|
||||
|
||||
def test_each_board_heads_its_page(self) -> None:
|
||||
# The page's title names the board, no h2 says it again (round 5, 6.8).
|
||||
for name in BOARDS:
|
||||
slug = toc_slug(name)
|
||||
html = _page(DIST / "systems" / f"arcade-{slug}" / "index.html")
|
||||
self.assertRegex(html, rf'<h2[^>]*id="{re.escape(slug)}"', name)
|
||||
self.assertRegex(html, rf"<h1[^>]*>Arcade: {re.escape(name)}</h1>", name)
|
||||
self.assertNotRegex(html, rf"<h2[^>]*>{re.escape(name)}<", name)
|
||||
|
||||
def test_every_board_on_an_emulator_systems_row_is_a_link(self) -> None:
|
||||
if not (DIST / "emulators").is_dir():
|
||||
@@ -223,7 +225,10 @@ class PartPagesKnowWhereTheyAre(unittest.TestCase):
|
||||
orphans, numbered = [], []
|
||||
for page in sorted((CONTENT / "systems").glob("*.md*")):
|
||||
text = page.read_text(encoding="utf-8")
|
||||
if "Part of " in text and "\nparent:" not in text.split("\n---\n", 1)[0]:
|
||||
# A part is titled "<maker>: <console>" or "<name> files a to b".
|
||||
title = re.search(r"^title: (.*)$", text, re.M)
|
||||
part = title and (": " in title[1] or re.search(r" files .+ to ", title[1]))
|
||||
if part and "\nparent:" not in text.split("\n---\n", 1)[0]:
|
||||
orphans.append(page.name)
|
||||
if re.search(r"^title: .*part \d+ of \d+", text, re.M | re.I):
|
||||
numbered.append(page.name)
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
"""System pages and boards (round 5, package 6).
|
||||
|
||||
NAOMI opened on hod2bios.zip, a set 13 of the 19 emulators listing it read
|
||||
for one game. Lines read other names for one file as files to get: "Start
|
||||
with: fdsbios.zip, disksys.rom". Twelve neogeo.zip were on five pages, the
|
||||
SNK ones closed under Variants. The Atom, two MZ computers, a sound chip and
|
||||
a toy had board pages, and the NAOMI and Atomiswave sets under the
|
||||
Dreamcast folder stayed off their boards' pages. ScummVM's files were on
|
||||
three pages, one section named ".variants". Board pages said their name
|
||||
three times before the first file.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import types
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
|
||||
import generate_site as gs # noqa: E402
|
||||
import siterender # noqa: E402
|
||||
|
||||
CONTENT = REPO_ROOT / "website" / "src" / "content" / "docs"
|
||||
SYSTEMS = CONTENT / "systems"
|
||||
|
||||
|
||||
def _file(name: str, char: str, path: str = "") -> dict:
|
||||
return {"name": name, "sha1": char * 40, "md5": char * 32, "size": 4096,
|
||||
"path": path or f"bios/Sega/NAOMI/{name}"}
|
||||
|
||||
|
||||
def _consumers(reads: dict[str, list[tuple[str, dict]]]) -> types.SimpleNamespace:
|
||||
"""Consumers where each sha1 is read by (emulator, entry) pairs, an
|
||||
emulator's entries kept in one list as a profile keeps them."""
|
||||
files_of: dict[str, list] = {}
|
||||
entries: dict[str, dict[str, int]] = {}
|
||||
emulators: dict[str, set[str]] = {}
|
||||
for sha1, read_by in reads.items():
|
||||
for emulator, entry in read_by:
|
||||
files_of.setdefault(emulator, []).append(entry)
|
||||
entries.setdefault(sha1, {})[emulator] = len(files_of[emulator]) - 1
|
||||
emulators.setdefault(sha1, set()).add(emulator)
|
||||
return types.SimpleNamespace(
|
||||
platforms={}, emulators=emulators, emulator_entries=entries,
|
||||
emulator_files=files_of, installed={}, platform_systems={},
|
||||
platform_destinations={}, _db={"files": {}},
|
||||
)
|
||||
|
||||
|
||||
def _page(stem: str) -> str:
|
||||
path = SYSTEMS / f"{stem}.md"
|
||||
if not path.is_file():
|
||||
raise unittest.SkipTest(f"systems/{stem}.md not generated")
|
||||
return path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _start_line(text: str) -> str:
|
||||
match = re.search(r"^\*\*(Start with|No file needed)\*\*.*$", text, re.M)
|
||||
return match[0] if match else ""
|
||||
|
||||
|
||||
def _database() -> dict:
|
||||
path = REPO_ROOT / "database.json"
|
||||
if not path.is_file():
|
||||
raise unittest.SkipTest("database.json is not built")
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
class AStartFileIsNoOnesExtra(unittest.TestCase):
|
||||
"""6.1: a file most of whose readers take beside another one is no
|
||||
start file, whoever requires it."""
|
||||
|
||||
def test_the_board_opens_on_its_bios_alone(self) -> None:
|
||||
naomi, hod2 = _file("naomi.zip", "1"), _file("hod2bios.zip", "2")
|
||||
both = [(f"flycast{n}", {"name": "hod2bios.zip", "required": True}) for n in range(2)]
|
||||
extra = [(f"mame{n}", {"name": "hod2bios.zip", "required": False}) for n in range(3)]
|
||||
consumers = _consumers({
|
||||
"1" * 40: [(e, {"name": "naomi.zip", "required": True}) for e, _ in both + extra],
|
||||
"2" * 40: both + extra,
|
||||
})
|
||||
self.assertEqual(gs._start_with([naomi, hod2], consumers),
|
||||
["**Start with**: `naomi.zip`.", ""])
|
||||
|
||||
def test_the_naomi_page_opens_on_naomi_zip(self) -> None:
|
||||
self.assertEqual(_start_line(_page("arcade-sega-naomi")), "**Start with**: `naomi.zip`.")
|
||||
|
||||
def test_a_file_several_require_beside_the_lead_stays(self) -> None:
|
||||
bios7, bios9 = _file("bios7.bin", "3"), _file("bios9.bin", "4")
|
||||
consumers = _consumers({
|
||||
"3" * 40: [(e, {"name": "bios7.bin", "required": True}) for e in ("a", "b")],
|
||||
"4" * 40: [(e, {"name": "bios9.bin", "required": True}) for e in ("a", "b")],
|
||||
})
|
||||
self.assertEqual(gs._start_with([bios7, bios9], consumers)[0],
|
||||
"**Start with**: `bios7.bin`, `bios9.bin`.")
|
||||
|
||||
def test_some_files_each_is_not_every_file_for_one(self) -> None:
|
||||
"""The SNES said bsnes and four more require 22 files."""
|
||||
files = [_file(f"chip{n}.rom", str(n)) for n in range(1, 4)]
|
||||
reads = {f["sha1"]: [(f"free{k}", {"name": f["name"], "required": False,
|
||||
"hle_fallback": True}) for k in range(5)]
|
||||
for f in files}
|
||||
reads["1" * 40].append(("bsnes", {"name": "chip1.rom", "required": True}))
|
||||
reads["2" * 40].append(("jgenesis", {"name": "chip2.rom", "required": True}))
|
||||
reads["3" * 40].append(("jgenesis", {"name": "chip3.rom", "required": True}))
|
||||
line = gs._start_with(files, _consumers(reads))[0]
|
||||
self.assertIn("require some of these files: ", line)
|
||||
|
||||
|
||||
class OtherNamesForOneSlotReadAsAlternatives(unittest.TestCase):
|
||||
"""6.2: "Start with: fdsbios.zip, disksys.rom" read as two files."""
|
||||
|
||||
def test_the_other_name_is_read_instead(self) -> None:
|
||||
disk, fds = _file("disksys.rom", "5"), _file("fdsbios.zip", "6")
|
||||
consumers = _consumers({
|
||||
"5" * 40: [(e, {"name": "disksys.rom", "required": True})
|
||||
for e in ("mesen", "nestopia", "fceumm")],
|
||||
"6" * 40: [("fbneo", {"name": "fdsbios.zip", "required": True})],
|
||||
})
|
||||
self.assertEqual(gs._start_with([disk, fds], consumers)[0],
|
||||
"**Start with**: `disksys.rom`. FinalBurn Neo reads `fdsbios.zip` instead.")
|
||||
|
||||
def test_a_card_read_under_another_name_says_both(self) -> None:
|
||||
disk, card = _file("disksys.rom", "5"), _file("long name (1986).bin", "7")
|
||||
consumers = _consumers({
|
||||
"5" * 40: [(e, {"name": "disksys.rom", "required": True}) for e in ("a", "b")],
|
||||
"7" * 40: [("c", {"name": "short.rom", "required": True})],
|
||||
})
|
||||
self.assertIn("reads `long name (1986).bin` (as `short.rom`) instead.",
|
||||
gs._start_with([disk, card], consumers)[0])
|
||||
|
||||
def test_a_regional_line_names_one_file_per_reader(self) -> None:
|
||||
mega_cd = _page("sega").split("{ #mega-cd }", 1)[1].split("\n## ", 1)[0]
|
||||
regions = [line for line in mega_cd.splitlines() if line.startswith("- **")]
|
||||
self.assertTrue(regions)
|
||||
for line in regions:
|
||||
lead = line.split(". ", 1)[0]
|
||||
self.assertEqual(len(re.findall(r"`[^`]+`", lead)), 1, line)
|
||||
|
||||
def test_the_fds_line(self) -> None:
|
||||
fds = _page("nintendo").split("{ #famicom-disk-system }", 1)[1]
|
||||
self.assertRegex(_start_line(fds), r"^\*\*Start with\*\*: `disksys.rom`\. FinalBurn Neo "
|
||||
r"reads \[?`fdsbios.zip`.* instead\.$")
|
||||
|
||||
|
||||
class TheNeoGeoSetsSitWithTheNeoGeo(unittest.TestCase):
|
||||
"""6.3: every neogeo.zip has its card on SNK's pages, side by side and
|
||||
open, and the ares sentence names the card."""
|
||||
|
||||
def test_every_neogeo_zip_is_on_snk(self) -> None:
|
||||
db = _database()
|
||||
sha1s = [sha1 for sha1, f in db["files"].items() if f["name"] == "neogeo.zip"]
|
||||
held = "".join(path.read_text(encoding="utf-8") for path in SYSTEMS.glob("snk*.md"))
|
||||
if not held:
|
||||
self.skipTest("SNK pages not generated")
|
||||
self.assertEqual([s for s in sha1s if f'id="sha1-{s[:12]}"' not in held], [])
|
||||
others = "".join(path.read_text(encoding="utf-8")
|
||||
for path in SYSTEMS.glob("arcade*.md"))
|
||||
self.assertEqual([s for s in sha1s if f'id="sha1-{s[:12]}"' in others], [])
|
||||
|
||||
def test_the_sets_are_open_and_together(self) -> None:
|
||||
section = _page("snk").split("{ #neo-geo }", 1)[1].split("\n## ", 1)[0]
|
||||
titles = re.findall(r'class="rb-file-title"[^>]*><code>([^<]+)</code>', section)
|
||||
sets = [n for n, name in enumerate(titles) if name == "neogeo.zip"]
|
||||
self.assertGreater(len(sets), 1)
|
||||
self.assertEqual(sets, list(range(len(sets))))
|
||||
variants = section.split("Variants (", 1)[1] if "Variants (" in section else ""
|
||||
self.assertNotIn("<code>neogeo.zip</code>", variants)
|
||||
|
||||
def test_ares_is_told_the_card_it_reads(self) -> None:
|
||||
self.assertIn("ares reads `neo-epo.bin` and `sp-45.sp1` instead.", _page("snk"))
|
||||
|
||||
|
||||
class ABoardIsAnArcadeMachine(unittest.TestCase):
|
||||
"""6.4: computers, a sound chip and a toy had board pages."""
|
||||
|
||||
def test_no_board_has_a_parent_outside_arcade(self) -> None:
|
||||
wrong = sorted(board for board in siterender.arcade_boards()
|
||||
if board in siterender.SYSTEM_PARENTS)
|
||||
self.assertEqual(wrong, [])
|
||||
|
||||
def test_no_board_page_for_a_machine_filed_elsewhere(self) -> None:
|
||||
if not SYSTEMS.is_dir():
|
||||
self.skipTest("pages not generated")
|
||||
for stem in ("arcade-acorn-atom", "arcade-sharp-mz-2000", "arcade-sharp-mz-2500",
|
||||
"arcade-yamaha-ym2608", "arcade-miuchiz"):
|
||||
self.assertFalse((SYSTEMS / f"{stem}.md").exists(), stem)
|
||||
|
||||
def test_the_computers_sets_are_on_their_makers_page(self) -> None:
|
||||
acorn = _page("acorn")
|
||||
self.assertIn("{ #atom }", acorn)
|
||||
self.assertIn("<code>atom_gdos.zip</code>", acorn)
|
||||
cards = re.findall(r'class="rb-file-title"[^>]*><code>([^<]+)</code>', _page("sharp"))
|
||||
self.assertEqual(cards.count("mz2000.zip"), 2)
|
||||
|
||||
|
||||
class TheDreamcastBoardsAreFiledByBoard(unittest.TestCase):
|
||||
"""6.5: the NAOMI and Atomiswave sets under the Dreamcast folder."""
|
||||
|
||||
def test_the_board_sets_left_the_dreamcast(self) -> None:
|
||||
sega = _page("sega")
|
||||
dreamcast = sega.split("{ #dreamcast }", 1)[1].split("\n## ", 1)[0]
|
||||
for name in ("naomi.zip", "awbios.zip", "naomi2.zip"):
|
||||
self.assertNotIn(f"<code>{name}</code>", dreamcast)
|
||||
self.assertIn("(/retrobios/systems/arcade-sega-naomi/)", dreamcast)
|
||||
self.assertIn("(/retrobios/systems/arcade-sammy-atomiswave/)", dreamcast)
|
||||
|
||||
def test_every_naomi_set_is_on_its_board(self) -> None:
|
||||
db = _database()
|
||||
page = _page("arcade-sega-naomi")
|
||||
missing = [sha1 for sha1, f in db["files"].items()
|
||||
if f["name"] == "naomi.zip" and f'id="sha1-{sha1[:12]}"' not in page]
|
||||
self.assertEqual(missing, [])
|
||||
|
||||
def test_the_dreamcast_arcade_id_leads_to_naomi(self) -> None:
|
||||
siterender.reset_console_pages()
|
||||
for board in siterender.arcade_boards():
|
||||
siterender.record_board_section(board)
|
||||
try:
|
||||
self.assertEqual(siterender.system_section("sega-dreamcast-arcade"),
|
||||
("arcade", "Sega NAOMI"))
|
||||
finally:
|
||||
siterender.reset_console_pages()
|
||||
|
||||
|
||||
class EnginesGoByEngine(unittest.TestCase):
|
||||
"""6.6: no section named after a folder of variants or a file."""
|
||||
|
||||
def test_no_section_or_row_is_a_folder_or_a_file(self) -> None:
|
||||
db = _database()
|
||||
names = set(db["indexes"]["by_name"])
|
||||
index = _page("index")
|
||||
rows = re.findall(r"^\| \[([^\]]+)\]\(", index, re.M)
|
||||
headings = [h.strip() for path in SYSTEMS.glob("*.md")
|
||||
for h in re.findall(r"^## ([^{\n]+)", path.read_text(encoding="utf-8"), re.M)]
|
||||
wrong = sorted({n for n in rows + headings if n.startswith(".") or n in names})
|
||||
self.assertEqual(wrong, [])
|
||||
|
||||
def test_scummvm_has_one_page(self) -> None:
|
||||
self.assertFalse((SYSTEMS / "other-scummvm.md").exists())
|
||||
self.assertIn("## ScummVM {", _page("scummvm"))
|
||||
self.assertNotIn("](/retrobios/systems/other/#scummvm)", _page("index"))
|
||||
|
||||
def test_a_folder_named_by_a_profile_key_reads_the_engine(self) -> None:
|
||||
index = _page("index")
|
||||
self.assertIn("| [EasyRPG Player](/retrobios/systems/rpg-maker/#easyrpg) |", index)
|
||||
self.assertIn("| [SDLPAL](", index)
|
||||
|
||||
def test_moved_pages_redirect(self) -> None:
|
||||
if not SYSTEMS.is_dir():
|
||||
self.skipTest("pages not generated")
|
||||
for old, new in gs.SYSTEM_PAGES_MOVED.items():
|
||||
self.assertFalse((CONTENT / old).exists(), old)
|
||||
self.assertTrue((CONTENT / new).exists(), new)
|
||||
redirects = CONTENT.parents[1] / "generated" / "redirects.json"
|
||||
if redirects.is_file():
|
||||
table = json.loads(redirects.read_text(encoding="utf-8"))
|
||||
self.assertEqual(table.get("/systems/other-scummvm/"), "/systems/scummvm/")
|
||||
|
||||
|
||||
class OneLabelOnceOnABoardPage(unittest.TestCase):
|
||||
"""6.7, 6.8, 6.9, 6.10: the words a board or part page repeats."""
|
||||
|
||||
def test_a_label_naming_files_installs_these_files(self) -> None:
|
||||
block = "\n".join(gs._console_install_block("Other arcade files", [("mame", "arcade")]))
|
||||
self.assertIn("<summary>Install these files</summary>", block)
|
||||
block = "\n".join(gs._console_install_block("Sega NAOMI", [("mame", "arcade")]))
|
||||
self.assertIn("<summary>Install the files of Sega NAOMI</summary>", block)
|
||||
|
||||
def test_a_board_page_has_no_heading_of_its_name(self) -> None:
|
||||
page = _page("arcade-sega-naomi")
|
||||
self.assertNotRegex(page, r"^## Sega NAOMI", )
|
||||
self.assertNotIn("](/retrobios/systems/arcade-sega-naomi/#sega-naomi)",
|
||||
_page("index"))
|
||||
|
||||
def test_no_part_says_where_it_sits(self) -> None:
|
||||
if not SYSTEMS.is_dir():
|
||||
self.skipTest("pages not generated")
|
||||
said = sorted(p.name for p in SYSTEMS.glob("*.md")
|
||||
if "\nPart of [" in p.read_text(encoding="utf-8"))
|
||||
self.assertEqual(said, [])
|
||||
|
||||
def test_a_part_goes_by_the_emulators_name(self) -> None:
|
||||
titles = [re.search(r"^title: (.*)$", p.read_text(encoding="utf-8"), re.M)[1].strip("'")
|
||||
for p in SYSTEMS.glob("other-mame*.md")]
|
||||
if not titles:
|
||||
self.skipTest("pages not generated")
|
||||
self.assertEqual([t for t in titles if not t.startswith(("MAME files", "Other: MAME"))],
|
||||
[])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user