mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-24 12:49:03 -05:00
1101 lines
43 KiB
Python
1101 lines
43 KiB
Python
"""Small renderings shared by every page."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import urllib.parse
|
|
from collections.abc import Callable
|
|
from pathlib import Path
|
|
|
|
import sitemarkup as markup
|
|
|
|
|
|
WIKI_SRC_DIR = "wiki" # manually maintained wiki sources
|
|
SYSTEM_ICON_BASE = "https://raw.githubusercontent.com/libretro/retroarch-assets/master/xmb/systematic/png"
|
|
# Local copies of the upstream icons, named as upstream names them.
|
|
ICON_DIR = Path(__file__).resolve().parent.parent / "docs_assets" / "system-icons"
|
|
|
|
# Icon names the site holds a copy of. A name mapped to False has none.
|
|
_icon_available: dict[str, bool] = {}
|
|
|
|
_CODE_SPAN = re.compile(r"(`+).+?\1", re.S)
|
|
|
|
|
|
def _code(value: object) -> str:
|
|
"""A value as an inline code span it cannot leave.
|
|
|
|
Markdown shows HTML inside a code span as text, so the one way out is a
|
|
backtick in the value closing the span early. Data from a profile, a
|
|
platform file or a file name is not ours to trust that far.
|
|
"""
|
|
return "`" + str(value).replace("`", "'") + "`"
|
|
|
|
|
|
def _count(number: int, noun: str, plural: str = "") -> str:
|
|
"""A count and its noun, singular for one: the site printed "1 files" 234 times."""
|
|
return f"{number:,} {noun if number == 1 else plural or noun + 's'}"
|
|
|
|
|
|
def _escape_tags(text: str) -> str:
|
|
"""Show a placeholder such as ``<system_dir>`` as it was written.
|
|
|
|
Profile prose names paths with angle brackets. Passed through as HTML
|
|
they are unknown tags and vanish from the page, and ``<title id>`` opens
|
|
a title element that swallows everything after it. Code spans keep their
|
|
brackets, and an autolink stays a link.
|
|
"""
|
|
def outside(chunk: str) -> str:
|
|
return re.sub(r"<(?!https?://|mailto:)", "<", chunk)
|
|
|
|
out: list[str] = []
|
|
position = 0
|
|
for match in _CODE_SPAN.finditer(text):
|
|
out.append(outside(text[position:match.start()]))
|
|
out.append(match.group(0))
|
|
position = match.end()
|
|
out.append(outside(text[position:]))
|
|
return "".join(out)
|
|
|
|
|
|
def _admonition_body(text: str) -> str:
|
|
"""Indent prose without turning source tokens such as ``#if`` into H1s."""
|
|
escaped = re.sub(r"(?m)^(\s*)#", r"\1\\#", _escape_tags(text))
|
|
return escaped.replace("\n", "\n ")
|
|
|
|
def _icon_name(manufacturer: str, console_name: str) -> str:
|
|
return f"{manufacturer} - {console_name}".replace("/", " ")
|
|
|
|
def _icon_url(icon_name: str) -> str:
|
|
return f"{SYSTEM_ICON_BASE}/{urllib.parse.quote(icon_name)}.png"
|
|
|
|
def prime_system_icons(names: set[str]) -> None:
|
|
"""Record which system icons the site holds a copy of.
|
|
|
|
The icons are copied once into ``docs_assets/system-icons/`` by
|
|
``scripts/fetch_site_images.py``: the build reads that folder and never
|
|
the network. A system without a copy gets no icon, because a heading
|
|
with a broken image reads worse than a heading without one.
|
|
"""
|
|
held = {path.stem for path in ICON_DIR.glob("*.png")}
|
|
_icon_available.update({name: name in held for name in names})
|
|
missing = sum(1 for name in names if name not in held)
|
|
if missing:
|
|
print(f" {missing} systems have no icon in {ICON_DIR.relative_to(ICON_DIR.parents[1])}")
|
|
|
|
def system_icon_markdown(manufacturer: str, console_name: str) -> str:
|
|
"""Icon image for a system heading, empty when the site holds none.
|
|
|
|
The link names the upstream file: ``sitediagrams.localize_images``
|
|
points it at the local copy, whose source is recorded beside it.
|
|
"""
|
|
name = _icon_name(manufacturer, console_name)
|
|
if not _icon_available.get(name):
|
|
return ""
|
|
return markup.image(console_name, _icon_url(name), width="24") + " "
|
|
|
|
# Global index: maps system_id -> (manufacturer_slug, console_name) for cross-linking
|
|
_system_page_map: dict[str, tuple[str, str]] = {}
|
|
|
|
def _slugify_anchor(text: str) -> str:
|
|
"""Slugify text for MkDocs anchor compatibility."""
|
|
import re
|
|
|
|
slug = text.lower()
|
|
slug = re.sub(r"[^\w\s-]", "", slug)
|
|
slug = re.sub(r"[\s]+", "-", slug)
|
|
slug = slug.strip("-")
|
|
return slug
|
|
|
|
_norm_page_map: dict[str, tuple[str, str] | None] = {}
|
|
|
|
|
|
def set_system_page_map(mapping: dict[str, tuple[str, str]]) -> None:
|
|
"""Install the system id -> page table every link on the site reads.
|
|
|
|
The table replaces the previous one, and the index of other spellings is
|
|
derived from it here. Kept anywhere else, it outlived a table of the same
|
|
size and linked to the pages of the previous one.
|
|
"""
|
|
from common import _norm_system_id
|
|
|
|
_system_page_map.clear()
|
|
_system_page_map.update(mapping)
|
|
_norm_page_map.clear()
|
|
for known, page in _system_page_map.items():
|
|
key = _norm_system_id(known)
|
|
if _norm_page_map.get(key, page) != page:
|
|
_norm_page_map[key] = None
|
|
else:
|
|
_norm_page_map[key] = page
|
|
|
|
|
|
def _page_by_norm(sys_id: str) -> tuple[str, str] | None:
|
|
"""The page an id leads to under another spelling of the same system.
|
|
|
|
Emulator cards name sega-megadrive or sinclair-zxspectrum, the pages are
|
|
keyed by the platforms' spellings: 2,431 of 4,195 cards had no link. A
|
|
spelling two pages share leads nowhere rather than to either.
|
|
"""
|
|
from common import _norm_system_id
|
|
|
|
return _norm_page_map.get(_norm_system_id(sys_id))
|
|
|
|
|
|
# A manufacturer page too large for one page is a summary, each console on
|
|
# pages of its own: (slug, console) -> its page stems under systems/, and
|
|
# (slug, console, sha1) -> the stem holding that card. A console of a page
|
|
# kept whole has no entry, its section is on the manufacturer's page.
|
|
_console_pages: dict[tuple[str, str], list[str]] = {}
|
|
_card_pages: dict[tuple[str, str, str], str] = {}
|
|
|
|
|
|
def reset_console_pages() -> None:
|
|
_console_pages.clear()
|
|
_card_pages.clear()
|
|
|
|
|
|
def record_console_pages(
|
|
slug: str, console: str, stems: list[str], sha1s: list[list[str]]
|
|
) -> None:
|
|
"""Where a split manufacturer page put one console: its page stems, in
|
|
order, and the sha1 of each card on each of them."""
|
|
_console_pages[(slug, console)] = list(stems)
|
|
for stem, part in zip(stems, sha1s):
|
|
for sha1 in part:
|
|
_card_pages.setdefault((slug, console, sha1), stem)
|
|
|
|
|
|
def split_system_stems() -> set[str]:
|
|
"""Every page stem of a console moved to a page of its own."""
|
|
return {stem for stems in _console_pages.values() for stem in stems}
|
|
|
|
|
|
# (manufacturer slug, console) -> the id of the console's heading. Two
|
|
# folders spelled alike (Sega's "Mega CD" and "Mega-CD") share a slug, and
|
|
# the second heading took "mega-cd_1" while every link said "mega-cd".
|
|
_console_anchors: dict[tuple[str, str], str] = {}
|
|
|
|
|
|
def _manufacturer_slug(manufacturer: str) -> str:
|
|
return manufacturer.lower().replace(" ", "-")
|
|
|
|
|
|
def console_anchor(manufacturer: str, console: str) -> str:
|
|
"""The id of a console's heading on its manufacturer's page."""
|
|
from sitestarlight import toc_slug
|
|
|
|
key = (_manufacturer_slug(manufacturer), console)
|
|
return _console_anchors.get(key) or toc_slug(console)
|
|
|
|
|
|
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)."""
|
|
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)}"
|
|
|
|
|
|
def card_page(path: str, sha1: str) -> str:
|
|
"""The systems/ page stem holding the card of the copy at ``path``
|
|
(bios/<Manufacturer>/<Console>/...)."""
|
|
parts = path.split("/")
|
|
slug = parts[1].lower().replace(" ", "-")
|
|
console = parts[2] if len(parts) > 2 else ""
|
|
return _card_pages.get((slug, console, sha1), slug)
|
|
|
|
|
|
# Emulator cards moved off a page too long for one: (emulator, anchor) ->
|
|
# the page stem under emulators/ that holds the card.
|
|
_emulator_card_pages: dict[tuple[str, str], str] = {}
|
|
|
|
|
|
def record_emulator_cards(emulator: str, stem: str, anchors: list[str]) -> None:
|
|
for anchor in anchors:
|
|
_emulator_card_pages[(emulator, anchor)] = stem
|
|
|
|
|
|
def emulator_card_page(emulator: str, anchor: str) -> str:
|
|
"""The emulators/ page stem holding a card: the emulator's own page, or
|
|
the page its cards went to."""
|
|
return _emulator_card_pages.get((emulator, anchor), emulator)
|
|
|
|
|
|
def system_section(sys_id: str) -> tuple[str, str] | None:
|
|
"""The console section a system id leads to, as (manufacturer slug,
|
|
console): its own section, else the parent SYSTEM_PARENTS names, else
|
|
the one section whose heading reads system_name(). The folder a
|
|
system's files sit in is no answer by itself: Emerson's Arcadia 2001
|
|
led to the Saturn and Sega's NAOMI to the Dreamcast.
|
|
|
|
A heading under Arcade counts when it reads the name exactly (Namco
|
|
System 246). One under Other never does, its headings name programs."""
|
|
own = _own_section(sys_id)
|
|
if own is not None:
|
|
return _manufacturer_slug(own[0]), own[1]
|
|
parent = SYSTEM_PARENTS.get(sys_id)
|
|
if parent is not None and parent in _section_pairs:
|
|
return _manufacturer_slug(parent[0]), parent[1]
|
|
try:
|
|
name = system_name(sys_id)
|
|
except KeyError:
|
|
return None
|
|
places = _sections_by_name.get(name.casefold(), set())
|
|
if len(places) != 1:
|
|
return None
|
|
mfr, console = next(iter(places))
|
|
if mfr in _NAMELESS_TOPS or (
|
|
mfr in _PROGRAM_TOPS and console.casefold() in _program_names
|
|
):
|
|
return None
|
|
return _manufacturer_slug(mfr), console
|
|
|
|
|
|
def _system_link(sys_id: str, prefix: str = "") -> str:
|
|
"""A system id linked to the section named after it, or the bare id."""
|
|
section = system_section(sys_id)
|
|
if section:
|
|
slug, console = section
|
|
return f"[{sys_id}]({console_href(slug, console, prefix)})"
|
|
return sys_id
|
|
|
|
def _field_text(key: object, value: object) -> str:
|
|
"""A scalar of a structured block. A source reference reads as code."""
|
|
if isinstance(value, bool):
|
|
text = "yes" if value else "no"
|
|
elif value is None:
|
|
text = "none"
|
|
else:
|
|
# One line: a folded note's second line started at column 0 and
|
|
# left the admonition it was written in. Never cut: 34 notes lost
|
|
# their end, and a cut citation links to another line.
|
|
text = " ".join(str(value).split())
|
|
return f"`{text}`" if key == "source_ref" else text
|
|
|
|
|
|
def _field_label(key: object) -> str:
|
|
"""A YAML key as the page shows it: words, not an identifier."""
|
|
return str(key).replace("_", " ")
|
|
|
|
|
|
FieldText = Callable[[object, object], str]
|
|
|
|
|
|
def _render_yaml_value(
|
|
lines: list[str], val: object, indent: int = 4,
|
|
field_text: FieldText = _field_text,
|
|
) -> None:
|
|
"""Render any YAML value as a nested markdown list.
|
|
|
|
Every level is a list item and its children a sublist four spaces in.
|
|
A key written as a paragraph with its children indented below it read as
|
|
an indented code block: the clownmdemu page showed the raw asterisks.
|
|
``field_text`` renders each scalar, so a page can link a source_ref.
|
|
"""
|
|
pad = " " * indent
|
|
if isinstance(val, dict):
|
|
for k, v in val.items():
|
|
if isinstance(v, (dict, list)) and v:
|
|
lines.append(f"{pad}- **{_field_label(k)}:**")
|
|
lines.append("")
|
|
_render_yaml_value(lines, v, indent + 4, field_text)
|
|
else:
|
|
lines.append(f"{pad}- **{_field_label(k)}:** {field_text(k, v)}")
|
|
lines.append("")
|
|
elif isinstance(val, list):
|
|
for item in val:
|
|
if isinstance(item, dict):
|
|
# Scalars on the item's line, lists and maps as its children:
|
|
# cemu's firmware titles lost the files each title carries.
|
|
scalars = {ik: iv for ik, iv in item.items()
|
|
if not (isinstance(iv, (dict, list)) and iv)}
|
|
nested = {ik: iv for ik, iv in item.items() if ik not in scalars}
|
|
parts = [f"{_field_label(ik)}: {field_text(ik, iv)}"
|
|
for ik, iv in scalars.items()]
|
|
lines.append(f"{pad}- {', '.join(parts)}".rstrip())
|
|
if nested:
|
|
lines.append("")
|
|
_render_yaml_value(lines, nested, indent + 4, field_text)
|
|
else:
|
|
lines.append(f"{pad}- {field_text(None, item)}")
|
|
lines.append("")
|
|
elif isinstance(val, str) and "\n" in val:
|
|
for line in val.split("\n"):
|
|
lines.append(f"{pad}{line}")
|
|
else:
|
|
lines.append(f"{pad}{field_text(None, val)}")
|
|
|
|
|
|
def _platform_link(name: str, display: str, prefix: str = "") -> str:
|
|
"""Generate a markdown link to a platform page."""
|
|
return f"[{display}]({prefix}platforms/{name}.md)"
|
|
|
|
def _emulator_link(name: str, prefix: str = "") -> str:
|
|
"""Generate a markdown link to an emulator page."""
|
|
return f"[{name}]({prefix}emulators/{name}.md)"
|
|
|
|
def _fmt_size(size: int) -> str:
|
|
if size >= 1024 * 1024 * 1024:
|
|
return f"{size / (1024**3):.1f} GB"
|
|
if size >= 1024 * 1024:
|
|
return f"{size / (1024**2):.1f} MB"
|
|
if size >= 1024:
|
|
return f"{size / 1024:.1f} KB"
|
|
return f"{size} B"
|
|
|
|
def _pct(n: int, total: int) -> str:
|
|
if total == 0:
|
|
return "0%"
|
|
return f"{n / total * 100:.1f}%"
|
|
|
|
def _by_mode(value: str | dict, mode: str) -> str:
|
|
"""Read a profile field that may be keyed by build mode."""
|
|
if isinstance(value, dict):
|
|
return str(value.get(mode) or "") if mode else ""
|
|
return str(value or "")
|
|
|
|
|
|
# System display names. A system is named by the heading of the console
|
|
# section that is its own: the folder its id spells, under the maker the id
|
|
# names when two makers file a console of that name. The page a system's
|
|
# files were voted onto is not its section and never names it. That vote
|
|
# called nintendo-gba "Nintendo Game Boy" and camplynx "Arcade". A system
|
|
# with no section of its own is named in SYSTEM_NAMES, and a system in
|
|
# neither stops the build.
|
|
|
|
# Folders that file copies by the emulator or board that loads them. Their
|
|
# headings name a program, not a system.
|
|
_PROGRAM_TOPS = frozenset({"Other", "Arcade"})
|
|
# Of those, the top whose headings never name a system, even read whole.
|
|
# An Arcade heading can (Namco System 246), an Other one cannot.
|
|
_NAMELESS_TOPS = frozenset({"Other"})
|
|
|
|
# Every (manufacturer, console) section the system pages carry.
|
|
_section_pairs: set[tuple[str, str]] = set()
|
|
# Normalized id -> the console sections spelled that way.
|
|
_console_sections: dict[str, set[tuple[str, str]]] = {}
|
|
# A console section by the name its heading reads (Bandai WonderSwan).
|
|
_sections_by_name: dict[str, set[tuple[str, str]]] = {}
|
|
# Emulator and engine names. A heading spelled like one (Game Engines >
|
|
# Solarus, ZC > ZC210) files that program's copies and names no system.
|
|
_program_names: set[str] = set()
|
|
|
|
|
|
def program_names(profiles: dict[str, dict]) -> set[str]:
|
|
"""Every name an emulator profile goes by: its key, its emulator and
|
|
display names."""
|
|
names = {str(key) for key in profiles}
|
|
for profile in profiles.values():
|
|
names.update(
|
|
str(profile[field]) for field in ("emulator", "display_name")
|
|
if profile.get(field)
|
|
)
|
|
return names
|
|
|
|
|
|
def set_console_sections(
|
|
sections: dict[str, dict] | set[tuple[str, str]],
|
|
programs: set[str] | frozenset[str] = frozenset(),
|
|
) -> None:
|
|
"""Install the console sections the system pages carry.
|
|
|
|
``sections`` maps each manufacturer to its consoles, as the system pages
|
|
group them, or is a set of (manufacturer, console) pairs. ``programs``
|
|
are the emulator names, which never name a system.
|
|
"""
|
|
from common import _norm_system_id
|
|
|
|
pairs = (
|
|
{(mfr, console) for mfr, consoles in sections.items() for console in consoles}
|
|
if isinstance(sections, dict) else set(sections)
|
|
)
|
|
from sitestarlight import _unique, toc_slug
|
|
|
|
_console_sections.clear()
|
|
_sections_by_name.clear()
|
|
_console_anchors.clear()
|
|
_section_pairs.clear()
|
|
_section_pairs.update(pairs)
|
|
by_maker: dict[str, list[str]] = {}
|
|
for mfr, console in pairs:
|
|
by_maker.setdefault(mfr, []).append(console)
|
|
for name in {_heading_name(mfr, console).casefold(), console.casefold()}:
|
|
_sections_by_name.setdefault(name, set()).add((mfr, console))
|
|
# In the order the page lists them, after the page title's own id.
|
|
for mfr, consoles in by_maker.items():
|
|
used = {toc_slug(mfr)}
|
|
for console in sorted(consoles):
|
|
_console_anchors[(_manufacturer_slug(mfr), console)] = _unique(toc_slug(console), used)
|
|
for mfr, console in pairs:
|
|
spelled = console.lower().replace(" ", "-")
|
|
slug = mfr.lower().replace(" ", "-")
|
|
for key in {_norm_system_id(f"{slug}-{spelled}"), _norm_system_id(spelled)}:
|
|
_console_sections.setdefault(key, set()).add((mfr, console))
|
|
_program_names.clear()
|
|
_program_names.update(name.casefold() for name in programs)
|
|
_sections_installed[0] = True
|
|
|
|
|
|
_sections_installed = [False]
|
|
_REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
|
|
|
|
def _ensure_console_sections() -> None:
|
|
"""Read the sections from the repository when no generator installed
|
|
them, so a page rendered on its own names systems as the site does."""
|
|
if _sections_installed[0]:
|
|
return
|
|
import json
|
|
|
|
from common import load_emulator_profiles
|
|
from generate_site import _group_by_manufacturer
|
|
|
|
with open(_REPO_ROOT / "database.json", encoding="utf-8") as handle:
|
|
db = json.load(handle)
|
|
profiles = load_emulator_profiles(str(_REPO_ROOT / "emulators"), skip_aliases=False)
|
|
set_console_sections(_group_by_manufacturer(db), program_names(profiles))
|
|
|
|
|
|
def _own_section(sys_id: str) -> tuple[str, str] | None:
|
|
"""The console section that is this id's own, or None.
|
|
|
|
Of two sections spelled alike (Atari and Camputers both file a Lynx),
|
|
the one under the maker the id names is kept. Two left is no answer,
|
|
and neither is a section that files a program's copies.
|
|
"""
|
|
from common import _norm_system_id
|
|
|
|
_ensure_console_sections()
|
|
places = _console_sections.get(_norm_system_id(sys_id), set())
|
|
if len(places) > 1:
|
|
places = {
|
|
(mfr, console) for mfr, console in places
|
|
if sys_id.lower().startswith(mfr.lower().replace(" ", "-") + "-")
|
|
}
|
|
if len(places) != 1:
|
|
return None
|
|
mfr, console = next(iter(places))
|
|
# A folder under a program's top, named like its own page (Dinothawr) or
|
|
# like an emulator holds a program's files, not a system's.
|
|
if (
|
|
mfr in _PROGRAM_TOPS
|
|
or console.casefold() == mfr.casefold()
|
|
or _heading_name(mfr, console).casefold() in _program_names
|
|
):
|
|
return None
|
|
return mfr, console
|
|
|
|
|
|
def _heading_name(mfr: str, console: str) -> str:
|
|
"""A console heading read out of its page: the maker in front, unless
|
|
the heading already starts with it or the page is a game publisher's."""
|
|
from common import GAME_DATA_TOPS
|
|
|
|
if mfr in GAME_DATA_TOPS:
|
|
return console
|
|
if console.casefold().startswith(mfr.split()[0].casefold()):
|
|
return console
|
|
return f"{mfr} {console}"
|
|
|
|
|
|
def system_name(sys_id: str) -> str:
|
|
"""The display name of a system id, the only one a page prints."""
|
|
sys_id = str(sys_id)
|
|
section = _own_section(sys_id)
|
|
if section is not None:
|
|
return _heading_name(*section)
|
|
name = SYSTEM_NAMES.get(sys_id)
|
|
if name is None:
|
|
raise KeyError(
|
|
f"system {sys_id!r} has no console section of its own and no "
|
|
"entry in siterender.SYSTEM_NAMES"
|
|
)
|
|
return name
|
|
|
|
|
|
# Systems without a console section of their own.
|
|
SYSTEM_NAMES: dict[str, str] = {
|
|
"2ship": "The Legend of Zelda: Majora's Mask",
|
|
"aarch64": "ARM 64-bit (AArch64)",
|
|
"acclaim-zn1": "Acclaim ZN-1",
|
|
"adrift": "ADRIFT",
|
|
"advsys": "AdvSys",
|
|
"ags": "Adventure Game Studio games",
|
|
"agt": "Adventure Game Toolkit",
|
|
"alan": "Alan",
|
|
"alg-laserdisc": "American Laser Games laserdisc",
|
|
"allied-leisure-pinball": "Allied Leisure pinball",
|
|
"alpha": "DEC Alpha",
|
|
"andamiro-pumpitup": "Andamiro Pump It Up",
|
|
"apf-m1000": "APF M-1000",
|
|
"apfm1000": "APF M-1000",
|
|
"apple-iie": "Apple IIe",
|
|
"apple-ios": "Apple iOS",
|
|
"apple2": "Apple II",
|
|
"apple2gs": "Apple IIGS",
|
|
"arcade": "Arcade games",
|
|
"arcade-laserdisc": "Laserdisc arcade games",
|
|
"arcadia-systems": "Arcadia Systems Multi Select",
|
|
"arduboy": "Arduboy",
|
|
"aristocrat-mk5": "Aristocrat MK5",
|
|
"aristocrat-mk6": "Aristocrat MK6",
|
|
"aristocrat-mk7": "Aristocrat MK7",
|
|
"arm": "ARM",
|
|
"astrocde": "Bally Astrocade",
|
|
"atari-falcon": "Atari Falcon",
|
|
"atari-jaguarcd": "Atari Jaguar CD",
|
|
"atari-ste": "Atari STE",
|
|
"atari-system1": "Atari System 1",
|
|
"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",
|
|
"barcrest-mpu4": "Barcrest MPU4",
|
|
"bbc-master-128": "Acorn BBC Master 128",
|
|
"bbc-master-compact": "Acorn BBC Master Compact",
|
|
"bbc-master-et": "Acorn BBC Master ET",
|
|
"bbc-micro-b": "Acorn BBC Micro Model B",
|
|
"bbc-micro-bplus": "Acorn BBC Micro Model B+",
|
|
"bbk-longman-4980": "BBK Longman A4980",
|
|
"bbk-longman-4988": "BBK Longman A4988",
|
|
"bennugd": "BennuGD games",
|
|
"bermuda-syndrome": "Bermuda Syndrome game",
|
|
"brezzasoft-crystal": "BrezzaSoft Crystal System",
|
|
"bshift": "Half-Life: Blue Shift",
|
|
"bsyndrome": "Bermuda Syndrome game",
|
|
"caesar3": "Caesar III",
|
|
"cambridge-z88": "Cambridge Computer Z88",
|
|
"camplynx": "Camputers Lynx",
|
|
"capcom-cps1": "Capcom CPS-1",
|
|
"capcom-cps2": "Capcom CPS-2",
|
|
"capcom-cps3": "Capcom CPS-3",
|
|
"capcom-zn1": "Capcom ZN-1",
|
|
"capcom-zn2": "Capcom ZN-2",
|
|
"casio-pv1000": "Casio PV-1000",
|
|
"catacomb-3d": "Catacomb 3-D",
|
|
"catacomb-adventure": "Catacomb Adventure Series",
|
|
"cave-cv1000": "Cave CV1000",
|
|
"cave-story": "Cave Story",
|
|
"cavestory": "Cave Story",
|
|
"celeste-classic": "Celeste Classic",
|
|
"century-cvs": "Century CVS",
|
|
"ces-galaxy-games": "CES Galaxy Games",
|
|
"cgenie": "EACA Colour Genie",
|
|
"chex": "Chex Quest",
|
|
"chihiro": "Sega Chihiro",
|
|
"chip8": "CHIP-8",
|
|
"coco3": "Tandy Color Computer 3",
|
|
"commanderx16": "Commander X16",
|
|
"commodore-c64-supercpu": "Commodore 64 with SuperCPU",
|
|
"commodore-cd32": "Commodore Amiga CD32",
|
|
"commodore-cdtv": "Commodore CDTV",
|
|
"commodore-cubo": "CD Express Cubo CD32",
|
|
"coolbar": "Coolbar feature phones",
|
|
"cps1": "Capcom CPS-1",
|
|
"cps2": "Capcom CPS-2",
|
|
"cps3": "Capcom CPS-3",
|
|
"crvision": "VTech CreatiVision",
|
|
"cstrike": "Counter-Strike",
|
|
"ctf": "Quake II: Capture the Flag",
|
|
"czero": "Counter-Strike: Condition Zero",
|
|
"czeror": "Counter-Strike: Condition Zero Deleted Scenes",
|
|
"dark-forces": "Star Wars: Dark Forces",
|
|
"dark-forces-ii": "Star Wars Jedi Knight: Dark Forces II",
|
|
"deco-cassette": "Data East DECO Cassette System",
|
|
"descent2": "Descent II",
|
|
"dingoo-a320": "Dingoo A320",
|
|
"dinothawr": "Dinothawr game",
|
|
"discrete-arcade": "Discrete logic arcade games",
|
|
"dmc": "Deathmatch Classic",
|
|
"doom3-bfg": "Doom 3: BFG Edition",
|
|
"dos": "MS-DOS",
|
|
"dragon32": "Dragon 32",
|
|
"dragon64": "Dragon 64",
|
|
"droidworks": "Star Wars: DroidWorks",
|
|
"duke-nukem-2": "Duke Nukem II",
|
|
"duke3d": "Duke Nukem 3D",
|
|
"dusklight": "The Legend of Zelda: Twilight Princess",
|
|
"efo-cedar-magnet": "EFO Cedar Magnet System",
|
|
"eighting-zn1": "Eighting ZN-1",
|
|
"elektor-tv-games-computer": "Elektor TV Games Computer",
|
|
"emerson-arcadia-2001": "Emerson Arcadia 2001",
|
|
"enemy-territory-quake-wars": "Enemy Territory: Quake Wars",
|
|
"enemyterritory": "Wolfenstein: Enemy Territory",
|
|
"enterprise-128": "Enterprise 128",
|
|
"enterprise-64": "Enterprise 64",
|
|
"entex-advision": "Entex Adventure Vision",
|
|
"epoch-scv": "Epoch Super Cassette Vision",
|
|
"epochcv": "Epoch Cassette Vision",
|
|
"eternal-computer": "Eternal Software Initiative computer",
|
|
"exelvision-exl100": "Exelvision EXL 100",
|
|
"exhumed": "PowerSlave",
|
|
"exidy-maxaflex": "Exidy Max-A-Flex",
|
|
"fallout": "Fallout",
|
|
"famicom": "Nintendo Famicom",
|
|
"flash": "Adobe Flash",
|
|
"flashback": "Flashback",
|
|
"fm77av": "Fujitsu FM77AV",
|
|
"fpinball": "Future Pinball tables",
|
|
"fruitmachine": "Fruit machines",
|
|
"fury": "Ion Fury game",
|
|
"gaelco-powervr": "Gaelco PowerVR boards",
|
|
"galaksija": "Galaksija computer",
|
|
"game": "Picross puzzle (libretro sample)",
|
|
"gamecom": "Tiger Game.com",
|
|
"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",
|
|
"gottlieb-system1": "Gottlieb System 1",
|
|
"half-life-2": "Half-Life 2",
|
|
"handheld-electronic": "Nintendo Game & Watch",
|
|
"heart-of-darkness": "Heart of Darkness",
|
|
"heretic": "Heretic",
|
|
"heroes-of-might-and-magic-2": "Heroes of Might and Magic II",
|
|
"hexen": "Hexen",
|
|
"hexen2": "Hexen II",
|
|
"hppa": "HP PA-RISC",
|
|
"hugo": "Hugo",
|
|
"hyper-neogeo64": "SNK Hyper Neo Geo 64",
|
|
"ibm-at": "IBM PC/AT",
|
|
"ibm-pcjr": "IBM PCjr",
|
|
"ibm-ps1": "IBM PS/1",
|
|
"ibm-xt": "IBM PC XT",
|
|
"icarus-starship-command-simulator": "Icarus Starship Command Simulator",
|
|
"igs-pgm": "IGS PolyGame Master",
|
|
"imax-macs": "I'Max MACS",
|
|
"interact": "Interact Home Computer",
|
|
"interton-vc4000": "Interton VC 4000",
|
|
"itech-eagle": "Incredible Technologies Eagle",
|
|
"jacl": "JACL",
|
|
"jedi-academy": "Star Wars Jedi Knight: Jedi Academy",
|
|
"jedi-outcast": "Star Wars Jedi Knight II: Jedi Outcast",
|
|
"jediacademy": "Star Wars Jedi Knight: Jedi Academy",
|
|
"jediknight2": "Star Wars Jedi Knight II: Jedi Outcast",
|
|
"jupiter-ace": "Jupiter Cantab Jupiter Ace",
|
|
"konami-bubble-system": "Konami Bubble System",
|
|
"konami-bubblesystem": "Konami Bubble System",
|
|
"konami-bubsys": "Konami Bubble System",
|
|
"konami-endeavour": "Konami Endeavour",
|
|
"konami-gq": "Konami GQ",
|
|
"konami-gv": "Konami GV",
|
|
"konami-gx": "Konami GX",
|
|
"konami-python": "Konami Python",
|
|
"konami-python2": "Konami Python 2",
|
|
"konami-twinkle": "Konami Twinkle",
|
|
"konami-viper": "Konami Viper",
|
|
"lambda-8300": "Lambda 8300",
|
|
"laserdisc-arcade": "Laserdisc arcade games",
|
|
"level9": "Level 9",
|
|
"lowresnx": "LowRes NX programs",
|
|
"lynx": "Atari Lynx",
|
|
"m68k": "Motorola 68000",
|
|
"magnetic-scrolls": "Magnetic Scrolls",
|
|
"marty": "Fujitsu FM Towns Marty",
|
|
"mega-cd-32x": "Sega Mega-CD 32X",
|
|
"mega-duck": "Welback Mega Duck",
|
|
"mega-ld": "Sega Mega LD",
|
|
"microdigital-tk85": "Microdigital TK85",
|
|
"microsoft-msx2": "MSX2",
|
|
"microsoft-xbox-360": "Microsoft Xbox 360",
|
|
"midway-mcr": "Midway MCR",
|
|
"mips": "MIPS",
|
|
"mips64": "MIPS64",
|
|
"miuchiz": "Miuchiz",
|
|
"mohaa": "Medal of Honor: Allied Assault",
|
|
"mohaa-breakthrough": "Medal of Honor: Allied Assault Breakthrough",
|
|
"mohaa-spearhead": "Medal of Honor: Allied Assault Spearhead",
|
|
"msx1": "MSX",
|
|
"msx2": "MSX2",
|
|
"msx2+": "MSX2+",
|
|
"msx2plus": "MSX2+",
|
|
"msxturboR": "MSX turbo R",
|
|
"msxturbor": "MSX turbo R",
|
|
"mysteries-of-the-sith": "Star Wars Jedi Knight: Mysteries of the Sith",
|
|
"myvision": "Nichibutsu My Vision",
|
|
"mz2000": "Sharp MZ-2000",
|
|
"mz2500": "Sharp MZ-2500",
|
|
"mz700": "Sharp MZ-700",
|
|
"mz800": "Sharp MZ-800",
|
|
"mz80k": "Sharp MZ-80K",
|
|
"nam": "NAM",
|
|
"namco-na1": "Namco NA-1",
|
|
"namco-nb1": "Namco NB-1",
|
|
"namco-system11": "Namco System 11",
|
|
"namco-system246": "Namco System 246",
|
|
"namco-system256": "Namco System 256",
|
|
"namco-system357": "Namco System 357",
|
|
"namco-system573": "Konami System 573",
|
|
"naomi2": "Sega NAOMI 2",
|
|
"napalm": "NAPALM",
|
|
"nec-pc-80": "NEC PC-8001",
|
|
"nec-supergrafx": "NEC SuperGrafx",
|
|
"neo-geo-pocket-color": "SNK Neo Geo Pocket Color",
|
|
"neogeo64": "SNK Hyper Neo Geo 64",
|
|
"ngp": "SNK Neo Geo Pocket",
|
|
"ngpc": "SNK Neo Geo Pocket Color",
|
|
"nichibutsu-hrdvd": "Nichibutsu High Rate DVD",
|
|
"nintendo-aleck64": "Nintendo Aleck64",
|
|
"nintendo-fds": "Nintendo Famicom Disk System",
|
|
"nintendo-gb": "Nintendo Game Boy",
|
|
"nintendo-gba": "Nintendo Game Boy Advance",
|
|
"nintendo-gbc": "Nintendo Game Boy Color",
|
|
"nintendo-nss": "Nintendo Super System",
|
|
"nintendo-playch10": "Nintendo PlayChoice-10",
|
|
"nintendo-sfcbox": "Nintendo Super Famicom Box",
|
|
"nintendo-sgb": "Nintendo Super Game Boy",
|
|
"nintendo-virtualboy": "Nintendo Virtual Boy",
|
|
"nintendo-wii-arcade": "Nintendo Wii arcade boards",
|
|
"nmk16": "NMK16",
|
|
"oberon": "Project Oberon",
|
|
"olivetti-pc128": "Olivetti Prodest PC 128",
|
|
"omicron": "Omicron cartridges",
|
|
"onscripter": "ONScripter visual novels",
|
|
"openbor": "OpenBOR games",
|
|
"opengoal": "Jak and Daxter",
|
|
"oric": "Tangerine Oric",
|
|
"oricatmos": "Oric Atmos",
|
|
"outlaws": "Outlaws",
|
|
"palm-os": "Palm OS",
|
|
"pc-engine-ld": "NEC PC Engine LD-ROM2",
|
|
"pc60": "NEC PC-6001",
|
|
"pc80": "NEC PC-8001",
|
|
"pce-tourvision": "TourVision",
|
|
"pdark": "Perfect Dark",
|
|
"pegasus": "Aamber Pegasus",
|
|
"pgm": "IGS PolyGame Master",
|
|
"philips-p2000t": "Philips P2000T",
|
|
"philips-vg5k": "Philips VG 5000",
|
|
"picocomputer-6502": "Picocomputer 6502 programs",
|
|
"pocket-challenge-v2": "Benesse Pocket Challenge V2",
|
|
"powerslave": "PowerSlave",
|
|
"ppc": "PowerPC",
|
|
"ppc64": "PowerPC 64-bit",
|
|
"prey": "Prey",
|
|
"prince-of-persia": "Prince of Persia",
|
|
"ps4": "Sony PlayStation 4",
|
|
"pv1000": "Casio PV-1000",
|
|
"quadrilateral-cowboy": "Quadrilateral Cowboy",
|
|
"quake2": "Quake II",
|
|
"quake3": "Quake III Arena",
|
|
"quake4": "Quake 4",
|
|
"realrtcw": "Return to Castle Wolfenstein (RealRTCW)",
|
|
"recel-system3": "Recel System III",
|
|
"redneck": "Redneck Rampage",
|
|
"ricochet": "Ricochet",
|
|
"ringo-r470": "Ringo R470",
|
|
"riscv": "RISC-V",
|
|
"riscv32": "RISC-V 32-bit",
|
|
"riscv64": "RISC-V 64-bit",
|
|
"rogue": "Quake II: Ground Zero",
|
|
"rott": "Rise of the Triad",
|
|
"rpgmaker": "RPG Maker games",
|
|
"s390x": "IBM z/Architecture (s390x)",
|
|
"sam-coupe": "MGT SAM Coupe",
|
|
"sammy-atomiswave": "Sammy Atomiswave",
|
|
"sammy-medal": "Sammy medal games",
|
|
"science-of-cambridge-mk14": "Science of Cambridge MK14",
|
|
"scott-adams": "Scott Adams adventures",
|
|
"scott-adams-plus": "Scott Adams Plus adventures",
|
|
"scummvm": "ScummVM games",
|
|
"scv": "Epoch Super Cassette Vision",
|
|
"sdlpal": "The Legend of Sword and Fairy",
|
|
"sega-atomiswave": "Sammy Atomiswave",
|
|
"sega-dreamcast-arcade": "Sega Dreamcast-based arcade boards",
|
|
"sega-genesis": "Sega Genesis",
|
|
"sega-isgsm": "Sega ISG Selection Master",
|
|
"sega-megaplay": "Sega Mega Play",
|
|
"sega-megatech": "Sega Mega-Tech",
|
|
"sega-model2": "Sega Model 2",
|
|
"sega-naomi": "Sega NAOMI",
|
|
"sega-naomi2": "Sega NAOMI 2",
|
|
"sega-naomigd": "Sega NAOMI GD-ROM",
|
|
"sega-outrun": "OutRun",
|
|
"sega-pico": "Sega Pico",
|
|
"sega-sg1000": "Sega SG-1000",
|
|
"sega-shootingzone": "Sega Shooting Zone",
|
|
"sega-system-sp": "Sega System SP",
|
|
"sega-system16b": "Sega System 16B",
|
|
"sega-systemsp": "Sega System SP",
|
|
"sega-vmu": "Sega Dreamcast VMU",
|
|
"serious-sam": "Serious Sam",
|
|
"sg-1000": "Sega SG-1000",
|
|
"sielectronics-y2": "SiElectronics System Y2",
|
|
"sinclair-ql": "Sinclair QL",
|
|
"sinclair-spectrum": "Sinclair ZX Spectrum",
|
|
"sinclair-zx-spectrum-next": "ZX Spectrum Next",
|
|
"sinclair-zx80-81": "Sinclair ZX80 and ZX81",
|
|
"skin-deep": "Skin Deep",
|
|
"smbx": "Super Mario Bros. X",
|
|
"sms": "Sega Master System",
|
|
"snk-neogeo-aes": "SNK Neo Geo AES",
|
|
"snk-neogeo-mvs": "SNK Neo Geo MVS",
|
|
"snk-ngp": "SNK Neo Geo Pocket",
|
|
"snk-ngpc": "SNK Neo Geo Pocket Color",
|
|
"soh": "The Legend of Zelda: Ocarina of Time",
|
|
"sonic-1-2013": "Sonic the Hedgehog (2013)",
|
|
"sonic-2-2013": "Sonic the Hedgehog 2 (2013)",
|
|
"sonic-mania": "Sonic Mania",
|
|
"soniccd": "Sonic CD",
|
|
"sony-psp": "Sony PlayStation Portable",
|
|
"sparc": "SPARC",
|
|
"sparc64": "SPARC64",
|
|
"spectravideo": "Spectravideo",
|
|
"star-control-2": "Star Control II",
|
|
"starship": "Star Fox 64",
|
|
"strife": "Strife",
|
|
"sunplus-native32": "Sunplus Native32",
|
|
"sunplus-spmp8000": "Sunplus SPMP8000",
|
|
"super-famicom": "Nintendo Super Famicom",
|
|
"super-kaneko-nova": "Kaneko Super Nova System",
|
|
"super-mario-war": "Super Mario War game",
|
|
"superacan": "Funtech Super A'Can",
|
|
"supergrafx": "NEC SuperGrafx",
|
|
"supergrafx-cd": "NEC SuperGrafx CD",
|
|
"solarus": "Solarus games",
|
|
"supertux": "SuperTux game",
|
|
"supertuxkart": "SuperTuxKart game",
|
|
"supracan": "Funtech Super A'Can",
|
|
"symbian": "Symbian OS",
|
|
"tab-silverball": "TAB Austria Silverball",
|
|
"tads": "TADS",
|
|
"taito-cchip": "Taito C-Chip",
|
|
"taito-fx1": "Taito FX-1",
|
|
"taito-gnet": "Taito G-NET",
|
|
"taito-type-x": "Taito Type X",
|
|
"taito-typezero": "Taito Type Zero",
|
|
"tamagotchi-p1": "Bandai Tamagotchi P1",
|
|
"tandy-1000": "Tandy 1000",
|
|
"taylormade": "TaylorMade",
|
|
"teamarena": "Quake III: Team Arena",
|
|
"tecmo-tps": "Tecmo TPS",
|
|
"tfc": "Team Fortress Classic",
|
|
"the-dark-mod": "The Dark Mod",
|
|
"thomson-mo6": "Thomson MO6",
|
|
"thomson-to7": "Thomson TO7",
|
|
"thomson-to770": "Thomson TO7/70",
|
|
"thomson-to8": "Thomson TO8",
|
|
"thomson-to8d": "Thomson TO8D",
|
|
"thomson-to9": "Thomson TO9",
|
|
"thomson-to9plus": "Thomson TO9+",
|
|
"tic80": "TIC-80 cartridges",
|
|
"tiger-game-com": "Tiger Game.com",
|
|
"timex-tc2048": "Timex Computer 2048",
|
|
"timex-ts1500": "Timex Sinclair 1500",
|
|
"timex-ts2068": "Timex Sinclair 2068",
|
|
"tomb-raider": "Tomb Raider",
|
|
"tomb-raider-1": "Tomb Raider",
|
|
"tomb-raider-2": "Tomb Raider II",
|
|
"transport-tycoon-deluxe": "Transport Tycoon Deluxe",
|
|
"tyrian": "Tyrian",
|
|
"ur-quan-masters": "Star Control II",
|
|
"urban-terror": "Urban Terror",
|
|
"uzebox": "Uzebox",
|
|
"vaporspec": "VaporSpec cartridges",
|
|
"vc4000": "Interton VC 4000",
|
|
"vg5k": "Philips VG 5000",
|
|
"vgmplay": "Video game music files",
|
|
"videobrain": "Umtech VideoBrain",
|
|
"videosystem-zn1": "Video System ZN-1",
|
|
"vircon32": "Vircon32 cartridges",
|
|
"virtuality-su2000": "Virtuality SU2000",
|
|
"vm-labs-nuon": "VM Labs Nuon",
|
|
"vpinball": "Visual Pinball tables",
|
|
"vtech-vsmile": "VTech V.Smile",
|
|
"vvvvvv": "VVVVVV game",
|
|
"watara-supervision": "Watara Supervision",
|
|
"wonderswan-color": "Bandai WonderSwan Color",
|
|
"ww2gi": "World War II GI",
|
|
"x86": "x86 PC",
|
|
"x86-64": "x86-64 PC",
|
|
"xatrix": "Quake II: The Reckoning",
|
|
"xrick": "Rick Dangerous",
|
|
"ym2608": "Yamaha YM2608",
|
|
"zc210": "Zelda Classic",
|
|
"zeldaclassic": "Zelda Classic",
|
|
"zn1-acclaim": "Acclaim ZN-1",
|
|
"zn1-atlus": "Atlus ZN-1",
|
|
"zn1-capcom": "Capcom ZN-1",
|
|
"zn1-taito": "Taito ZN-1",
|
|
"zn1-tecmo": "Tecmo ZN-1",
|
|
"zn1-video-system": "Video System ZN-1",
|
|
"zn2-capcom": "Capcom ZN-2",
|
|
"zx-spectrum-128": "Sinclair ZX Spectrum 128",
|
|
}
|
|
|
|
|
|
# A system id with no section of its own -> the (manufacturer, console)
|
|
# section holding its files: a model, a regional name or an add-on of the
|
|
# machine that section covers, or a game whose data that section files.
|
|
# Reviewed entry by entry against the files the profiles and platforms
|
|
# declare for the id, never decided by a vote of the folders those files
|
|
# sit in. A section that is not the machine stays out of this table even
|
|
# when it holds a copy: NAOMI's naomi2.zip filed with the Dreamcast, the
|
|
# PET's ROMs in VICE's tree under the C128, the Arcadia's under the Saturn.
|
|
SYSTEM_PARENTS: dict[str, tuple[str, str]] = {
|
|
# Nintendo. The twin folders (DS and Nintendo DS, 64DD and Nintendo
|
|
# 64DD) gave each name two headings and no link.
|
|
"64dd": ("Nintendo", "Nintendo 64DD"),
|
|
"nintendo-64dd": ("Nintendo", "Nintendo 64DD"),
|
|
"nintendo-ds": ("Nintendo", "Nintendo DS"),
|
|
"nintendo-dsi": ("Nintendo", "Nintendo DS"),
|
|
"famicom": ("Nintendo", "NES"),
|
|
"super-famicom": ("Nintendo", "SNES"),
|
|
# Bandai: the Color boot ROMs, and those of Benesse's WonderSwan-based
|
|
# Pocket Challenge V2, sit with the WonderSwan.
|
|
"wonderswan-color": ("Bandai", "WonderSwan"),
|
|
"bandai-wswanc": ("Bandai", "WonderSwan"),
|
|
"pocket-challenge-v2": ("Bandai", "WonderSwan"),
|
|
# SNK
|
|
"ngpc": ("SNK", "Neo Geo Pocket"),
|
|
"snk-ngpc": ("SNK", "Neo Geo Pocket"),
|
|
"neo-geo-pocket-color": ("SNK", "Neo Geo Pocket"),
|
|
"snk-neogeo-aes": ("SNK", "Neo Geo"),
|
|
"snk-neogeo-mvs": ("SNK", "Neo Geo"),
|
|
# Sega. The VMU is the Dreamcast's memory card, its images sit there.
|
|
# Sega's arcade section holds the OutRun board ROMs Cannonball reads.
|
|
"sega-genesis": ("Sega", "Mega Drive"),
|
|
"mega-cd-32x": ("Sega", "Mega CD"),
|
|
"sega-vmu": ("Sega", "Dreamcast"),
|
|
"sega-outrun": ("Sega", "Arcade"),
|
|
# NEC and Pioneer: both LaserActive packs (LD-ROM2 and Mega LD) file
|
|
# their BIOS under the LaserActive.
|
|
"nec-supergrafx": ("NEC", "PC Engine"),
|
|
"supergrafx": ("NEC", "PC Engine"),
|
|
"supergrafx-cd": ("NEC", "PC Engine CD"),
|
|
"pc-engine-ld": ("Pioneer", "LaserActive"),
|
|
"mega-ld": ("Pioneer", "LaserActive"),
|
|
# Microsoft MSX generations
|
|
"microsoft-msx2": ("Microsoft", "MSX"),
|
|
"msx2": ("Microsoft", "MSX"),
|
|
"msx2plus": ("Microsoft", "MSX"),
|
|
"msx2+": ("Microsoft", "MSX"),
|
|
"msxturboR": ("Microsoft", "MSX"),
|
|
"msxturbor": ("Microsoft", "MSX"),
|
|
# Sinclair: 128-0.rom and 128-1.rom, and Timex's tc2048.rom.
|
|
"zx-spectrum-128": ("Sinclair", "ZX Spectrum"),
|
|
"timex-tc2048": ("Sinclair", "ZX Spectrum"),
|
|
# Acorn: os12.rom and basic2.rom for the Model B and B+, and the
|
|
# Master Compact's bbcmc.zip.
|
|
"bbc-micro-b": ("Acorn", "BBC Micro"),
|
|
"bbc-micro-bplus": ("Acorn", "BBC Micro"),
|
|
"bbc-master-compact": ("Acorn", "BBC Micro"),
|
|
# Atari: falcon.img, ste.img and tt.img sit with the ST TOS images.
|
|
"atari-ste": ("Atari", "ST"),
|
|
"atari-tt": ("Atari", "ST"),
|
|
"atari-falcon": ("Atari", "ST"),
|
|
# Home computers filed under their family
|
|
"apple-iie": ("Apple", "Apple II"),
|
|
"apf-m1000": ("APF", "Imagination Machine"),
|
|
"apfm1000": ("APF", "Imagination Machine"),
|
|
"bbk-longman-4980": ("BBK", "A4980"),
|
|
"bbk-longman-4988": ("BBK", "A4988"),
|
|
"coco3": ("Tandy", "CoCo"),
|
|
"commodore-cdtv": ("Commodore", "Amiga"),
|
|
"dragon32": ("Dragon", "Dragon"),
|
|
"dragon64": ("Dragon", "Dragon"),
|
|
"enterprise-64": ("Enterprise", "64-128"),
|
|
"enterprise-128": ("Enterprise", "64-128"),
|
|
"fm77av": ("Fujitsu", "FM-7"),
|
|
"gamepock": ("Epoch", "Game Pocket"),
|
|
"ibm-pcjr": ("IBM", "PC"),
|
|
"ibm-xt": ("IBM", "PC"),
|
|
"marty": ("Fujitsu", "FM Towns"),
|
|
"mz700": ("Sharp", "MZ"),
|
|
"mz80k": ("Sharp", "MZ"),
|
|
"oricatmos": ("Oric", "Oric"),
|
|
"palm-os": ("Palm", "Palm"),
|
|
"philips-vg5k": ("Philips", "VG5000"),
|
|
"vg5k": ("Philips", "VG5000"),
|
|
"videobrain": ("VideoBrain", "Family Computer"),
|
|
# 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"),
|
|
"vircon32": ("Vircon", "Vircon32"),
|
|
# Games, filed with the game whose folder holds their data
|
|
"dinothawr": ("Dinothawr", "Dinothawr"),
|
|
"doom": ("Id Software", "Doom"),
|
|
"ctf": ("Id Software", "Quake II"),
|
|
"teamarena": ("Id Software", "Quake III Arena"),
|
|
"enemyterritory": ("Id Software", "Wolfenstein Enemy Territory"),
|
|
"gearbox": ("Valve", "Half-Life"),
|
|
"dmc": ("Valve", "Half-Life"),
|
|
"ricochet": ("Valve", "Half-Life"),
|
|
"tfc": ("Valve", "Half-Life"),
|
|
"mohaa": ("Electronic Arts", "Medal of Honor Allied Assault"),
|
|
"mohaa-breakthrough": ("Electronic Arts", "Medal of Honor Allied Assault"),
|
|
"mohaa-spearhead": ("Electronic Arts", "Medal of Honor Allied Assault"),
|
|
"ur-quan-masters": ("Toys for Bob", "The Ur-Quan Masters"),
|
|
"star-control-2": ("Toys for Bob", "The Ur-Quan Masters"),
|
|
# The collection's general arcade section, where naomi.zip, neogeo.zip
|
|
# and the other board sets sit.
|
|
"arcade": ("Arcade", "Arcade"),
|
|
}
|
|
|
|
|
|
def system_ids(profiles: dict[str, dict], platforms: list[dict]) -> set[str]:
|
|
"""Every system id a page can name: the platforms' and the profiles'."""
|
|
ids = {str(sys_id) for config in platforms for sys_id in config.get("systems", {})}
|
|
for profile in profiles.values():
|
|
ids.update(str(sys_id) for sys_id in profile.get("systems") or [])
|
|
for entry in profile.get("files") or []:
|
|
if not isinstance(entry, dict):
|
|
continue
|
|
if entry.get("system"):
|
|
ids.add(str(entry["system"]))
|
|
ids.update(str(sys_id) for sys_id in entry.get("systems") or [])
|
|
return ids
|
|
|
|
|
|
def link_coverage(ids: set[str]) -> tuple[int, int]:
|
|
"""How many of ``ids`` lead to a section, and how many there are."""
|
|
return sum(1 for sys_id in ids if system_section(sys_id)), len(ids)
|
|
|
|
|
|
# A system id -> the names the platforms' own files give it, built once.
|
|
_search_aliases: dict[str, list[str]] = {}
|
|
_search_aliases_built = [False]
|
|
# A native name as a person types it: no space, no comma. RetroArch's
|
|
# native names are No-Intro titles ("Sony - PlayStation") and stay out.
|
|
_ALIAS_SHAPE = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.+-]*")
|
|
|
|
|
|
def system_search_aliases(sys_id: str) -> list[str]:
|
|
"""The native system ids the platform YAMLs file ``sys_id`` under
|
|
(psx, dreamcast, gba), sorted, for search. Read from each platform's
|
|
own entries by common.platform_system_names, never typed by hand."""
|
|
if not _search_aliases_built[0]:
|
|
from common import (
|
|
list_registered_platforms,
|
|
load_platform_config,
|
|
platform_system_names,
|
|
)
|
|
|
|
platforms_dir = str(_REPO_ROOT / "platforms")
|
|
index: dict[str, set[str]] = {}
|
|
for platform in list_registered_platforms(platforms_dir, include_archived=True):
|
|
config = load_platform_config(platform, platforms_dir)
|
|
names = platform_system_names(config, list(config.get("systems", {})))
|
|
for owner, spelled in names.items():
|
|
index.setdefault(owner, set()).update(
|
|
name for name in spelled if _ALIAS_SHAPE.fullmatch(name)
|
|
)
|
|
# Search ignores case: one spelling per word, the lower-case one
|
|
# when a platform writes it (psx, not PSX).
|
|
_search_aliases.clear()
|
|
for owner, names in index.items():
|
|
spellings: dict[str, str] = {}
|
|
for name in sorted(names, key=lambda n: (n != n.lower(), n)):
|
|
spellings.setdefault(name.casefold(), name)
|
|
if spellings:
|
|
_search_aliases[owner] = sorted(spellings.values(), key=str.casefold)
|
|
_search_aliases_built[0] = True
|
|
return list(_search_aliases.get(str(sys_id), []))
|