"""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 ```` 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 ```` 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 is named after, as (manufacturer slug, console): its own section, else the one section whose heading reads system_name(). The folder a system's files sit in is no answer: Emerson's Arcadia 2001 led to the Saturn and Sega's NAOMI to the Dreamcast.""" own = _own_section(sys_id) if own is not None: return _manufacturer_slug(own[0]), own[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 _PROGRAM_TOPS: 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"}) # 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() 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", }