feat: name each system once, from its own section

siterender.system_name reads the heading of the console section the id spells, then SYSTEM_NAMES, and raises for an id in neither. The emulator intro and the picker titles read it instead of the folder a system's files were voted onto.
This commit is contained in:
Abdessamad Derraz committed 2026-10-23 02:04:06 +02:00
1 parent 2a50bc21f7
commit 72e7507ff9
4 files changed
+695 -10

No files matched your search

+9 -9
View File
@@ -4656,12 +4656,9 @@ def _emulator_intro(emu_name: str, profile: dict) -> str:
The page opened on a twelve-row table: "Type", "Version", "Checked by". The page opened on a twelve-row table: "Type", "Version", "Checked by".
DuckStation needs one PlayStation BIOS of any name, and the reader had DuckStation needs one PlayStation BIOS of any name, and the reader had
to infer it from the "Files found by" row.""" to infer it from the "Files found by" row."""
import siterender # noqa: PLC0415
names: list[str] = [] names: list[str] = []
for sys_id in profile.get("systems") or []: for sys_id in profile.get("systems") or []:
page = siterender._system_page_map.get(sys_id) or siterender._page_by_norm(sys_id) label = system_name(sys_id)
label = page[1] if page else str(sys_id)
if label not in names: if label not in names:
names.append(label) names.append(label)
files = [f for f in profile.get("files") or [] if isinstance(f, dict)] files = [f for f in profile.get("files") or [] if isinstance(f, dict)]
@@ -6675,6 +6672,7 @@ def _generate(args: argparse.Namespace) -> None:
_system_pages_from_data(manufacturers, coverages, db, profiles) _system_pages_from_data(manufacturers, coverages, db, profiles)
) )
print(f" {len(_system_page_map)} system IDs mapped to pages") print(f" {len(_system_page_map)} system IDs mapped to pages")
set_console_sections(manufacturers, program_names(profiles))
print("Generating system pages...") print("Generating system pages...")
@@ -6864,11 +6862,10 @@ def _generate(args: argparse.Namespace) -> None:
json.dumps(sitestarlight.sidebar(nav, present), indent=1) + "\n") json.dumps(sitestarlight.sidebar(nav, present), indent=1) + "\n")
write_if_changed(str(generated / "lastmod.json"), write_if_changed(str(generated / "lastmod.json"),
json.dumps(dict(sorted(lastmod.items())), indent=1) + "\n") json.dumps(dict(sorted(lastmod.items())), indent=1) + "\n")
mfr_names = {mfr.lower().replace(" ", "-"): mfr for mfr in manufacturers} system_ids = set(_system_page_map) | {
titles = {} sys_id for cov in coverages.values() for sys_id in cov["config"].get("systems", {})
for sys_id, (slug, console) in _system_page_map.items(): }
mfr = mfr_names.get(slug, "") titles = {sys_id: system_name(sys_id) for sys_id in sorted(system_ids)}
titles[sys_id] = console if console.startswith(mfr) else f"{mfr} {console}"
sitestarlight.picker_data(_REPO_ROOT / "install", public / "data" / "picker", titles) sitestarlight.picker_data(_REPO_ROOT / "install", public / "data" / "picker", titles)
total_pages = len(present) total_pages = len(present)
print(f"\nGenerated {total_pages} pages in {args.docs_dir}/") print(f"\nGenerated {total_pages} pages in {args.docs_dir}/")
@@ -6903,6 +6900,9 @@ from siterender import ( # noqa: E402,F401
_system_page_map, _system_page_map,
_system_link, _system_link,
set_system_page_map, set_system_page_map,
set_console_sections,
program_names,
system_name,
card_page, card_page,
console_href, console_href,
record_console_pages, record_console_pages,
+565
View File
@@ -306,3 +306,568 @@ def _by_mode(value: str | dict, mode: str) -> str:
if isinstance(value, dict): if isinstance(value, dict):
return str(value.get(mode) or "") if mode else "" return str(value.get(mode) or "") if mode else ""
return str(value or "") 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]]] = {}
# 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)
)
_console_sections.clear()
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",
"64dd": "Nintendo 64DD",
"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-cbm-ii": "Commodore CBM-II",
"commodore-cd32": "Commodore Amiga CD32",
"commodore-cdtv": "Commodore CDTV",
"commodore-cubo": "CD Express Cubo CD32",
"commodore-pet": "Commodore PET",
"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",
"doom": "Doom",
"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": "Sega Mega-CD",
"mega-cd-32x": "Sega Mega-CD 32X",
"mega-duck": "Welback Mega Duck",
"mega-ld": "Sega Mega LD",
"megacd": "Sega Mega-CD",
"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-64dd": "Nintendo 64DD",
"nintendo-aleck64": "Nintendo Aleck64",
"nintendo-ds": "Nintendo DS",
"nintendo-dsi": "Nintendo DSi",
"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-mega-cd": "Sega Mega-CD",
"sega-megacd": "Sega Mega-CD",
"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": "Sinclair ZX Spectrum",
"sinclair-zx-spectrum-next": "ZX Spectrum Next",
"sinclair-zx80-81": "Sinclair ZX80 and ZX81",
"sinclair-zxspectrum": "Sinclair ZX Spectrum",
"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",
"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": "Sinclair ZX Spectrum",
"zx-spectrum-128": "Sinclair ZX Spectrum 128",
}
+120
View File
@@ -0,0 +1,120 @@
"""One display name per system id (round 2, package P0).
An emulator page listed the folders a system's files were voted onto:
nintendo-gba read "Nintendo Game Boy", camplynx "Arcade", and MAME
"emulated FBNeo". siterender.system_name names a system from the console
section that is its own, then from SYSTEM_NAMES, and raises otherwise.
database.json carries no system id: its files reach a page through their
console folders, so the ids to name are the platforms' and the profiles'.
"""
from __future__ import annotations
import json
import sys
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT / "scripts"))
import siterender # noqa: E402
from common import ( # noqa: E402
list_registered_platforms,
load_emulator_profiles,
load_platform_config,
)
from generate_site import _group_by_manufacturer # noqa: E402
def _profile_system_ids(profiles: dict[str, dict]) -> set[str]:
ids: set[str] = set()
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
class SystemNames(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
with open(REPO_ROOT / "database.json", encoding="utf-8") as handle:
db = json.load(handle)
cls.manufacturers = _group_by_manufacturer(db)
cls.profiles = load_emulator_profiles(
str(REPO_ROOT / "emulators"), skip_aliases=False)
siterender.set_console_sections(
cls.manufacturers, siterender.program_names(cls.profiles))
platforms_dir = str(REPO_ROOT / "platforms")
cls.platform_ids = {
sys_id
for name in list_registered_platforms(platforms_dir, include_archived=True)
for sys_id in load_platform_config(name, platforms_dir).get("systems", {})
}
cls.profile_ids = _profile_system_ids(cls.profiles)
def _all_ids(self) -> set[str]:
return self.platform_ids | self.profile_ids
def test_every_system_id_has_a_name(self) -> None:
unnamed = []
for sys_id in sorted(self._all_ids()):
try:
name = siterender.system_name(sys_id)
except KeyError:
unnamed.append(sys_id)
continue
self.assertTrue(name.strip(), sys_id)
self.assertEqual(unnamed, [], "add these to siterender.SYSTEM_NAMES")
def test_no_name_is_a_manufacturer_page_or_an_emulator(self) -> None:
taken = {name.casefold() for name in self.manufacturers}
taken |= {name.casefold() for name in siterender.program_names(self.profiles)}
clashes = sorted(
(sys_id, siterender.system_name(sys_id))
for sys_id in self._all_ids()
if siterender.system_name(sys_id).casefold() in taken
)
self.assertEqual(clashes, [])
def test_table_entries_are_reached(self) -> None:
"""A table entry an own section shadows is never read, and an entry
no platform or profile names is a leftover."""
shadowed = sorted(
sys_id for sys_id in siterender.SYSTEM_NAMES
if siterender._own_section(sys_id) is not None
)
self.assertEqual(shadowed, [])
unused = sorted(set(siterender.SYSTEM_NAMES) - self._all_ids())
self.assertEqual(unused, [])
def test_names_that_read_a_folder_vote(self) -> None:
self.assertEqual(siterender.system_name("nintendo-gba"), "Nintendo Game Boy Advance")
self.assertEqual(siterender.system_name("camplynx"), "Camputers Lynx")
self.assertEqual(siterender.system_name("atari-lynx"), "Atari Lynx")
self.assertEqual(siterender.system_name("arcade"), "Arcade games")
self.assertEqual(siterender.system_name("sega-naomi"), "Sega NAOMI")
def test_own_section_names_from_its_heading(self) -> None:
self.assertEqual(siterender.system_name("nintendo-snes"), "Nintendo SNES")
self.assertEqual(siterender.system_name("sony-playstation"), "Sony PlayStation")
def test_an_unknown_id_raises(self) -> None:
with self.assertRaises(KeyError):
siterender.system_name("no-such-system-anywhere")
def test_mame_names_systems_not_emulators(self) -> None:
names = {siterender.system_name(s) for s in self.profiles["mame"].get("systems") or []}
self.assertNotIn("FBNeo", names)
self.assertNotIn("Arcade", names)
self.assertFalse(names & siterender.program_names(self.profiles))
if __name__ == "__main__":
unittest.main()
+1 -1
View File
@@ -56,7 +56,7 @@ Every module in `tests/`, by the area it guards.
| Handheld port | stubbed PortMaster dialogs, fake installer | `test_port` | | Handheld port | stubbed PortMaster dialogs, fake installer | `test_port` |
| Truth and native export | synthetic + cached originals | `test_export_counts`, `test_export_native_layer`, `test_export_report`, `test_exporters`, `test_truth_data_dir_readers`, `test_truth_diff`, `test_truth_layout` | | Truth and native export | synthetic + cached originals | `test_export_counts`, `test_export_native_layer`, `test_export_report`, `test_exporters`, `test_truth_data_dir_readers`, `test_truth_diff`, `test_truth_layout` |
| Scrapers, upstream and provenance | inline sources, synthetic | `test_check_freshness`, `test_dedup`, `test_elec_scrapers`, `test_fbneo_parser`, `test_find_pinned_revision`, `test_hash_merge`, `test_launchers`, `test_launchers_emudeck`, `test_launchers_recalbox`, `test_launchers_retrobat`, `test_launchers_retrodeck`, `test_launchers_retropie`, `test_launchers_rocknix`, `test_mame_devices`, `test_mame_parser`, `test_mame_ref_audit`, `test_profile_sync`, `test_provenance`, `test_refresh_data_dirs`, `test_refresh_failures`, `test_refresh_stale`, `test_retrodeck_scraper_errors`, `test_romset_recipes`, `test_scraper_contract`, `test_upstream`, `test_upstream_index` | | Scrapers, upstream and provenance | inline sources, synthetic | `test_check_freshness`, `test_dedup`, `test_elec_scrapers`, `test_fbneo_parser`, `test_find_pinned_revision`, `test_hash_merge`, `test_launchers`, `test_launchers_emudeck`, `test_launchers_recalbox`, `test_launchers_retrobat`, `test_launchers_retrodeck`, `test_launchers_retropie`, `test_launchers_rocknix`, `test_mame_devices`, `test_mame_parser`, `test_mame_ref_audit`, `test_profile_sync`, `test_provenance`, `test_refresh_data_dirs`, `test_refresh_failures`, `test_refresh_stale`, `test_retrodeck_scraper_errors`, `test_romset_recipes`, `test_scraper_contract`, `test_upstream`, `test_upstream_index` |
| Site, docs and repository contracts | rendered pages, sources | `test_contributing_text`, `test_coverage_cache`, `test_data_dir_refs`, `test_dataclass_defaults`, `test_doc_pages`, `test_doc_surfaces`, `test_optional_dependencies`, `test_prose_style`, `test_schema_semantic_gate`, `test_site_availability`, `test_site_content_data`, `test_site_copy`, `test_site_cores`, `test_site_emulator_page`, `test_site_exports`, `test_site_funnel`, `test_site_images`, `test_site_picker`, `test_site_platform_hashes`, `test_site_render`, `test_site_system_links`, `test_site_validation`, `test_site_weight`, `test_site_write`, `test_validate_pr`, `test_validate_pr_flow`, `test_validate_pr_hashes`, `test_workflow_paths` | | Site, docs and repository contracts | rendered pages, sources | `test_contributing_text`, `test_coverage_cache`, `test_data_dir_refs`, `test_dataclass_defaults`, `test_doc_pages`, `test_doc_surfaces`, `test_optional_dependencies`, `test_prose_style`, `test_schema_semantic_gate`, `test_site_availability`, `test_site_content_data`, `test_site_copy`, `test_site_cores`, `test_site_emulator_page`, `test_site_exports`, `test_site_funnel`, `test_site_images`, `test_site_picker`, `test_site_platform_hashes`, `test_site_r2_p0`, `test_site_render`, `test_site_system_links`, `test_site_validation`, `test_site_weight`, `test_site_write`, `test_validate_pr`, `test_validate_pr_flow`, `test_validate_pr_hashes`, `test_workflow_paths` |
## Test architecture ## Test architecture