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".
DuckStation needs one PlayStation BIOS of any name, and the reader had
to infer it from the "Files found by" row."""
import siterender # noqa: PLC0415
names: list[str] = []
for sys_id in profile.get("systems") or []:
page = siterender._system_page_map.get(sys_id) or siterender._page_by_norm(sys_id)
label = page[1] if page else str(sys_id)
label = system_name(sys_id)
if label not in names:
names.append(label)
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)
)
print(f" {len(_system_page_map)} system IDs mapped to pages")
set_console_sections(manufacturers, program_names(profiles))
print("Generating system pages...")
@@ -6864,11 +6862,10 @@ def _generate(args: argparse.Namespace) -> None:
json.dumps(sitestarlight.sidebar(nav, present), indent=1) + "\n")
write_if_changed(str(generated / "lastmod.json"),
json.dumps(dict(sorted(lastmod.items())), indent=1) + "\n")
mfr_names = {mfr.lower().replace(" ", "-"): mfr for mfr in manufacturers}
titles = {}
for sys_id, (slug, console) in _system_page_map.items():
mfr = mfr_names.get(slug, "")
titles[sys_id] = console if console.startswith(mfr) else f"{mfr} {console}"
system_ids = set(_system_page_map) | {
sys_id for cov in coverages.values() for sys_id in cov["config"].get("systems", {})
}
titles = {sys_id: system_name(sys_id) for sys_id in sorted(system_ids)}
sitestarlight.picker_data(_REPO_ROOT / "install", public / "data" / "picker", titles)
total_pages = len(present)
print(f"\nGenerated {total_pages} pages in {args.docs_dir}/")
@@ -6903,6 +6900,9 @@ from siterender import ( # noqa: E402,F401
_system_page_map,
_system_link,
set_system_page_map,
set_console_sections,
program_names,
system_name,
card_page,
console_href,
record_console_pages,
+565
View File
@@ -306,3 +306,568 @@ def _by_mode(value: str | dict, mode: str) -> str:
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]]] = {}
# 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` |
| 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` |
| 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