diff --git a/scripts/generate_site.py b/scripts/generate_site.py index 767275c7..0c6950c2 100644 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -4510,14 +4510,92 @@ def _group_by_manufacturer(db: dict) -> dict[str, dict[str, list]]: The database keeps one path per content, and a copy filed elsewhere was on no page of its own: the Channel F BIOS sat under Arcade > FBNeo only, and Fairchild had no system page. Every folder holding a copy lists it, - read from the full paths by_path_suffix records. + read from the full paths by_path_suffix records. The game engines' + files are grouped by engine (_engine_sections). """ files = db.get("files", {}) manufacturers: dict[str, dict[str, list]] = {} for sha1, where in _copy_places(db).items(): for manufacturer, console in sorted({(m, c) for m, c, _ in where}): manufacturers.setdefault(manufacturer, {}).setdefault(console, []).append(files[sha1]) - return manufacturers + return _engine_sections(manufacturers) + + +def _engine_sections(manufacturers: dict[str, dict[str, list]]) -> dict[str, dict[str, list]]: + """A game engine's files in one section headed with the engine's name. + + The Systems index listed ".variants", "scummvm.zip" and "wildmidi.cfg" + as games, and ScummVM's files sat on three pages. A folder of variants + joins the section of its primary, a file kept at a top's root joins + the section of the one engine that reads it (else of the engine the + top is named after), the folder under Other named after a game-engine + top joins that top, and a folder named by a profile key is headed with + the engine's name (SECTION_LABELS), its address unchanged. + """ + from siterender import _PROGRAM_TOPS, SECTION_LABELS, record_card_move # noqa: PLC0415 + + profiles = {key: profile for key, profile in _profiles_on_disk().items() + if profile.get("type") not in ("alias", "test")} + readers: dict[str, set[str]] = {} + for key, profile in profiles.items(): + for entry in profile.get("files") or []: + if isinstance(entry, dict): + for name in [entry.get("name"), *(entry.get("aliases") or [])]: + if name: + readers.setdefault(str(name).rsplit("/", 1)[-1], set()).add(key) + + def engine(key: str) -> str: + return str(profiles[key].get("emulator") or key) + + def profile_of(name: str) -> str | None: + return next((k for k in sorted(profiles) + if name.casefold() in (k.casefold(), engine(k).casefold())), None) + + out: dict[str, dict[str, list]] = {} + for mfr, consoles in manufacturers.items(): + if mfr not in GAME_DATA_TOPS: + out[mfr] = dict(consoles) + continue + own = profile_of(mfr) + regrouped: dict[str, list] = {} + for console, files in sorted(consoles.items(), key=lambda item: item[0].startswith(".")): + for f in files: + parts = str(f.get("path", "")).split("/") + target = console + if console.startswith(".") or (len(parts) == 3 and console == parts[-1]): + # A variant answers its primary's name: scummvm.zip.a93f1c4b. + base = parts[-1].rsplit(".", 1)[0] if console.startswith(".") else parts[-1] + keys = sorted(readers.get(base, ())) + key = keys[0] if len(keys) == 1 else own + found = next((c for c, held in regrouped.items() + if any(h.get("name") == base for h in held)), None) + target = found or (next((c for c in consoles if c == key), None) + or (engine(key) if key else base)) + if target != console: + record_card_move(mfr, console, str(f.get("sha1", "")), mfr, target) + regrouped.setdefault(target, []).append(f) + out[mfr] = regrouped + # The folder under Other an engine top is named after: ScummVM's + # engine data under Other/ScummVM, its themes under ScummVM/. + others = out.get("Other", {}) + for folder in sorted(others): + top = next((t for t in out if t in GAME_DATA_TOPS and t.casefold() == folder.casefold()), + None) + if top is None or top in _PROGRAM_TOPS: + continue + sections = out[top] + target = next((c for c in sections if c.casefold() == folder.casefold()), folder) + held = {str(f.get("sha1", "")) for f in sections.get(target, [])} + for f in others[folder]: + record_card_move("Other", folder, str(f.get("sha1", "")), top, target) + sections[target] = [*sections.get(target, []), + *(f for f in others.pop(folder) if str(f.get("sha1", "")) not in held)] + for mfr in GAME_DATA_TOPS: + for console in out.get(mfr, {}): + key = profiles.get(console) and console + if key and engine(key) != console: + SECTION_LABELS[(mfr, console)] = engine(key) + return {mfr: consoles for mfr, consoles in out.items() if consoles} def _console_display(manufacturer: str, console: str) -> str: @@ -4536,6 +4614,8 @@ def generate_systems_index(manufacturers: dict) -> str: PlayStation had to know it is filed under Sony, and Other hid 91 sections. Manufacturer pages stay the containers. """ + for old, new in SYSTEM_PAGES_MOVED.items(): + sitestarlight.MOVED_PAGES.setdefault(old, new) total_mfr = len(manufacturers) total_consoles = sum(len(c) for c in manufacturers.values()) # A copy filed under two folders appears on both pages, counted once here. @@ -4578,6 +4658,7 @@ def generate_systems_index(manufacturers: dict) -> str: rows = sorted( ((_folder_emulator(console)[0] if group == "programs" else _section_label(mfr, console) if mfr == "Arcade" + or (mfr, console) in SECTION_LABELS else _console_display(mfr, console), mfr, console, len(files)) for mfr, consoles in manufacturers.items() if kind(mfr) == group for console, files in consoles.items()), @@ -5561,10 +5642,29 @@ def _console_cards( if by_name: return (place.get(str(f.get("sha1", "")), _NO_PRIORITY), *_by_name(f)) return _card_rank(f, consumers, place) + # Another revision of a MAME or FBNeo set is another release's set, its + # Romset line saying which: it sits open beside the set of its name. + # Twelve neogeo.zip were on five pages, the SNK ones closed under + # Variants. + romsets = _romset_releases() + beside = [f for f in files if "/.variants/" in f["path"] and f["name"] in romsets] main_files = [f for f in files if "/.variants/" not in f["path"]] - variant_files = [f for f in files if "/.variants/" in f["path"]] + variant_files = [f for f in files if "/.variants/" in f["path"] and f not in beside] + # Sets of one name side by side, the first ranked leading. + ranked = sorted(main_files, key=order) + by_set: dict[str, list[dict]] = {} + for f in [*ranked, *sorted(beside, key=order)]: + if f["name"] in romsets: + by_set.setdefault(f["name"], []).append(f) + ordered: list[dict] = [] + for f in ranked: + if f["name"] not in romsets: + ordered.append(f) + elif by_set.get(f["name"]): + ordered.extend(by_set.pop(f["name"])) + ordered.extend(f for files in by_set.values() for f in files) cards = [(False, _system_file_card(f, consumers, anchored, searchable), str(f.get("sha1", ""))) - for f in sorted(main_files, key=order)] + for f in ordered] cards += [(True, _system_file_card(v, consumers, anchored, searchable), str(v.get("sha1", ""))) for v in sorted(variant_files, key=order)] return cards @@ -6275,6 +6375,23 @@ _BOARD_CONSOLES: dict[str, str] = {} # its maker, which said nothing about what it holds. OTHER_ARCADE = ("Arcade", "Arcade") OTHER_ARCADE_LABEL = "Other arcade files" +# (manufacturer, console) -> the boards whose sets left that folder's +# section for the boards' own pages. The section names them. +_BOARDS_FROM: dict[tuple[str, str], set[str]] = {} +# System pages published under an address no page holds since, and where +# their files went: the Atom and the MZ were boards, ScummVM's and SDLPAL's +# files were on two pages, the Dreamcast lost its NAOMI sets. An address +# redirects only while no page takes it back (sitestarlight.redirects). +SYSTEM_PAGES_MOVED = { + "systems/arcade-acorn-atom.md": "systems/acorn.md", + "systems/arcade-sharp-mz-2000.md": "systems/sharp.md", + "systems/arcade-sharp-mz-2500.md": "systems/sharp.md", + "systems/arcade-miuchiz.md": "systems/arcade.md", + "systems/arcade-yamaha-ym2608.md": "systems/arcade.md", + "systems/other-scummvm.md": "systems/scummvm.md", + "systems/other-sdlpal.md": "systems/sdlpal.md", + "systems/sega-dreamcast.md": "systems/sega.md", +} def arcade_board_sections( @@ -6287,18 +6404,23 @@ def arcade_board_sections( ZN-1 had no section, and their names stayed plain on 568 emulator pages. Each archive goes to the board siterender.arcade_board() reads from the profiles, never from its folder, and a folder named after a - board (Namco System 246) joins it. Files no board claims stay in their + board (Namco System 246) joins it, whatever maker's folder holds the + copy. A set a reviewed machine owns (siterender.archive_machine) goes + to that machine's section. Files neither claims stay in their folders. Each board is recorded (siterender.record_board_section) before any page links one, and a system's install commands follow its link to the board's section. Call after reset_console_pages().""" from siterender import ( # noqa: PLC0415 arcade_board, arcade_boards, + archive_machine, record_board_section, + record_card_move, system_section, ) _BOARD_CONSOLES.clear() + _BOARDS_FROM.clear() arcade = manufacturers.get("Arcade") if not arcade: return manufacturers, installs @@ -6307,23 +6429,66 @@ def arcade_board_sections( boards: dict[str, list] = {} folders: dict[str, list] = {} seen: dict[str, set[str]] = {} + # The sets a reviewed machine owns go to its section: neogeo.zip sat on + # five Arcade pages, the Atom's and the MZ-2000's sets on board pages. + machines: dict[tuple[str, str], list] = {} + + def file_on_board(board: str, f: dict) -> None: + # One card per content: a copy under Arcade and one under MAME + # are the same file. + if str(f.get("sha1", "")) in seen.setdefault(board, set()): + return + seen[board].add(str(f.get("sha1", ""))) + boards.setdefault(board, []).append(f) + for console in sorted(arcade): joined = by_name.get(console.casefold()) for f in arcade[console]: - board = joined or arcade_board(str(f.get("sha1", ""))) - if board is None: + sha1 = str(f.get("sha1", "")) + board = joined or arcade_board(sha1) + machine = None if board else archive_machine(sha1) + if board is not None: + file_on_board(board, f) + elif machine is not None: + machines.setdefault(machine, []).append(f) + record_card_move("Arcade", console, sha1, *machine) + else: folders.setdefault(console, []).append(f) - continue - # One card per content: a copy under Arcade and one under MAME - # are the same file. - if str(f.get("sha1", "")) in seen.setdefault(board, set()): - continue - seen[board].add(str(f.get("sha1", ""))) - boards.setdefault(board, []).append(f) + # A board's set filed under a machine's folder is on the board's page + # too, whatever the folder: naomi.zip under the Dreamcast was the copy + # five setups list, and the NAOMI page did not name it. + makers: dict[str, dict[str, list]] = {} + for mfr, consoles in manufacturers.items(): + if mfr == "Arcade": + continue + for console, files in consoles.items(): + kept = [] + for f in files: + sha1 = str(f.get("sha1", "")) + board = arcade_board(sha1) + machine = None if board else archive_machine(sha1) + if board is not None: + file_on_board(board, f) + _BOARDS_FROM.setdefault((mfr, console), set()).add(board) + elif machine is not None and machine != (mfr, console): + # The Atom's own set sat with the BBC Micro. + machines.setdefault(machine, []).append(f) + record_card_move(mfr, console, sha1, *machine) + else: + kept.append(f) + if len(kept) != len(files): + makers.setdefault(mfr, dict(consoles))[console] = kept + for (mfr, console), files in sorted(machines.items()): + consoles = makers.setdefault(mfr, dict(manufacturers.get(mfr, {}))) + held = {str(f.get("sha1", "")) for f in consoles.get(console, [])} + consoles[console] = [*consoles.get(console, []), + *(f for f in files if str(f.get("sha1", "")) not in held)] for board in sorted(boards): record_board_section(board) _BOARD_CONSOLES[names[board]] = board regrouped = {**folders, **{names[board]: files for board, files in boards.items()}} + makers = {mfr: {console: files for console, files in consoles.items() if files} + for mfr, consoles in makers.items()} moved: dict[str, dict[str, list[tuple[str, str]]]] = {} for slug, consoles in installs.items(): @@ -6335,7 +6500,21 @@ def arcade_board_sections( else: slug_to, console_to = slug, console moved.setdefault(slug_to, {}).setdefault(console_to, []).append((platform, sys_id)) - return {**manufacturers, "Arcade": regrouped}, moved + return {**manufacturers, **makers, "Arcade": regrouped}, moved + + +def _boards_line(manufacturer: str, console: str) -> list[str]: + """The line of a section whose board sets went to the boards' pages: + the Dreamcast folder holds NAOMI's and Atomiswave's.""" + from siterender import arcade_boards # noqa: PLC0415 + + boards = _BOARDS_FROM.get((manufacturer, console)) + if not boards: + return [] + names = arcade_boards() + links = [f"[{names[board]}]({console_href('arcade', names[board]).removeprefix('systems/')})" + for board in sorted(boards, key=lambda b: names[b].casefold())] + return [f"**Arcade board BIOS** on the boards' own pages: {', '.join(links)}.", ""] def _section_label(manufacturer: str, console: str) -> str: @@ -6446,7 +6625,8 @@ def system_pages( def start_of(console: str, first_below: bool = True) -> list[str]: if boards and console not in boards: return [] - return _start_with(consoles[console], consumers, first_below, (manufacturer, console)) + return [*_start_with(consoles[console], consumers, first_below, (manufacturer, console)), + *_boards_line(manufacturer, console)] starts = {console: [] if boards and console not in boards else _start_order(consoles[console], consumers, (manufacturer, console)) @@ -6605,9 +6785,12 @@ def system_pages( ]) lead = set(starts[console]) # A part by the files it holds: "Arcade: Arcade, part 3 of 5" said - # nothing a search result could use. - titles = ([f"{manufacturer}: {label}"] if len(stems) == 1 else [ - "{} files {} to {}".format(_part_name(manufacturer, console), *_part_ends(part, lead)) + # nothing a search result could use. A folder under Other goes by + # its emulator's name: "mame files" named the folder. + named = _folder_emulator(console)[0] if manufacturer == "Other" else None + titles = ([f"{manufacturer}: {named or label}"] if len(stems) == 1 else [ + "{} files {} to {}".format(named or _part_name(manufacturer, console), + *_part_ends(part, lead)) for part in parts]) count = _count(len(consoles[console]), "file") summary.extend(start_of(console, False)) @@ -6630,25 +6813,23 @@ def system_pages( following=neighbours[-1] if index + 1 < len(stems) else None, weight=_part_weight() if len(stems) > 1 else None, consoles=[label] if index == 0 and console in machines else None), - f"# {titles[index]}", "", - f"Part of [{manufacturer}]({slug}.md), in [Systems](index.md).", - "", *_icon_symbol(), + # The crumbs end on the maker's page: no line says it again. + f"# {titles[index]}", "", *_icon_symbol(), ] if len(stems) > 1: others = ", ".join( f"[part {i + 1}]({other}.md)" for i, other in enumerate(stems) if i != index) lines.extend([f"This part holds {_part_range(part, lead)}. The others: {others}.", ""]) - if board: - # The heading a board's links land on, above its start line. - lines.extend([f"## {label} {{ #{board_slug(board)} }}", ""]) + # A board's page is its one section: its title names it, and a + # heading of the same name said it a third time. Its cards come + # under "Files", as every page of one console's. if index == 0: lines.extend(start_of(console, len(stems) == 1)) lines.extend(legend) lines.extend(_romset_note({console: consoles[console]})) lines.extend(install_block(console)) - if not board: - lines.extend(["## Files", ""]) + lines.extend(["## Files", ""]) lines.extend(_cards_markdown(part)) lines.append("") lines.extend(_next_block([f"[{manufacturer}]({slug}.md), every section"] @@ -7500,8 +7681,12 @@ def _console_install_block(console: str, declared: list[tuple[str, str]]) -> lis ps = [line for platform, system in declared for line in ( f"# {_platform_display(platform)}", install_command("powershell", f"--platform {platform} --system {_ps_quote(system)}"))] - # The name last: "Install the Atari System 1 files" read as a count. - return search_excluded(markup.details("tip", f"Install the files of {console}", [ + # The name last: "Install the Atari System 1 files" read as a count. A + # label naming files already reads "Install these files": "Install + # the files of Other arcade files". + summary = ("Install these files" if console.casefold().endswith(" files") + else f"Install the files of {console}") + return search_excluded(markup.details("tip", summary, [ "Each platform names the system its own way.", "", "Linux, macOS, Steam Deck:", diff --git a/scripts/siterender.py b/scripts/siterender.py index c24ada69..9369b898 100644 --- a/scripts/siterender.py +++ b/scripts/siterender.py @@ -193,28 +193,44 @@ def console_anchor(manufacturer: str, console: str) -> str: def console_href(slug: str, console: str, prefix: str = "") -> str: """Where a console's files are: its own page, or its section of the manufacturer's page (also where a console cut into parts lists them). - An arcade board's section heads the page of its own name.""" + An arcade board's section is the page of its own name, no heading + repeating its title.""" if slug == "arcade" and _board_sections: board = _board_ids_by_name().get(console.casefold()) if board in _board_sections: - anchor = board_slug(board) - return f"{prefix}systems/arcade-{urllib.parse.quote(anchor)}.md#{anchor}" + return f"{prefix}systems/arcade-{urllib.parse.quote(board_slug(board))}.md" stems = _console_pages.get((slug, console)) if stems and len(stems) == 1: return f"{prefix}systems/{urllib.parse.quote(stems[0])}.md" return f"{prefix}systems/{urllib.parse.quote(slug)}.md#{console_anchor(slug, console)}" +# (manufacturer slug, folder, sha1) -> the (manufacturer slug, console) a +# copy's card went to when the system pages file it outside its folder: +# ScummVM's engine data under Other/ScummVM, neogeo.zip under Arcade. +_card_moves: dict[tuple[str, str, str], tuple[str, str]] = {} + + +def record_card_move(manufacturer: str, folder: str, sha1: str, + to_manufacturer: str, to_console: str) -> None: + """Mark that the card of the copy under bios/// + sits in the section (to_manufacturer, to_console).""" + _card_moves[(_manufacturer_slug(manufacturer), folder, str(sha1))] = ( + _manufacturer_slug(to_manufacturer), to_console) + + def card_page(path: str, sha1: str) -> str: """The systems/ page stem holding the card of the copy at ``path`` - (bios///...).""" + (bios///...). A board's set is on the board's + page whatever folder holds the copy.""" parts = path.split("/") slug = parts[1].lower().replace(" ", "-") console = parts[2] if len(parts) > 2 else "" - if slug == "arcade" and _board_sections: + if _board_sections: board = arcade_board(sha1) if board in _board_sections: return f"arcade-{board_slug(board)}" + slug, console = _card_moves.get((slug, console, str(sha1)), (slug, console)) return _card_pages.get((slug, console, sha1), slug) @@ -246,6 +262,10 @@ def system_section(sys_id: str) -> tuple[str, str] | None: A heading under Arcade counts when it reads the name exactly (Namco System 246). One under Other never does, its headings name programs.""" + if sys_id in BOARD_PARENTS and _board_sections: + board = _board_of_id(BOARD_PARENTS[sys_id]) + if board in _board_sections: + return "arcade", system_name(board) section = _section_without_boards(sys_id) if (not _board_sections or sys_id in SYSTEM_PARENTS or (section is not None and section[0] != "arcade")): @@ -301,6 +321,9 @@ _arcade_source: list[tuple[dict, dict] | None] = [None] _archive_boards: dict[str, str] = {} # Casefolded board name -> the board id that stands for it. _board_names: dict[str, str] = {} +# sha1 -> the (manufacturer, console) sections of the machines whose own +# set the archive is: neogeo.zip for the Neo Geo, mz2000.zip for the MZ. +_archive_machines: dict[str, set[tuple[str, str]]] = {} _boards_built = [False] @@ -392,6 +415,7 @@ def _build_boards() -> None: # sha1 -> board name -> the profiles naming it. None: a machine's own. votes: dict[str, dict[str, set[str]] | None] = {} + _archive_machines.clear() ids_by_name: dict[str, set[str]] = {} for key in sorted(profiles): profile = profiles[key] @@ -426,8 +450,18 @@ def _build_boards() -> None: for owner in owners: role = _board_role(owner) if role == _MACHINE: + # The profile's only system stands for an entry that names + # none: MAME's printer and expansion sets in a Dragon + # profile. Only an entry naming the machine moves its set, + # and only when every machine it names has that section: + # epson_fx80.zip, which sixteen machines load, has none. + declared = entry.get("system") or entry.get("systems") + places = {_machine_section(o) for o in owners + if _board_role(o) == _MACHINE} if declared else {None} + section = next(iter(places)) if len(places) == 1 else None for sha1 in archives: votes[sha1] = None + _archive_machines.setdefault(sha1, set()).add(section) continue if role is None: continue @@ -458,6 +492,27 @@ def _build_boards() -> None: _boards_built[0] = True +def _machine_section(sys_id: str) -> tuple[str, str] | None: + """The (manufacturer, console) section SYSTEM_PARENTS files a machine + id under, outside Arcade. Only a reviewed parent moves an archive out + of the folder holding it: an id with a section of its own keeps its + archives where the collection files them.""" + parent = SYSTEM_PARENTS.get(sys_id) + if parent is None or parent[0] == "Arcade" or parent not in _section_pairs: + return None + return parent + + +def archive_machine(sha1: str) -> tuple[str, str] | None: + """The (manufacturer, console) section of the machine an archive is + the set of, when a profile declares it for one machine only: the Neo + Geo's neogeo.zip, the MZ-2000's mz2000.zip. Read from the same + ``category: bios_zip`` entries as arcade_board(), never the folder.""" + _build_boards() + places = _archive_machines.get(str(sha1), set()) + return next(iter(places)) if len(places) == 1 else None + + def _board_ids_by_name() -> dict[str, str]: _build_boards() return _board_names @@ -674,6 +729,12 @@ def set_console_sections( {(mfr, console) for mfr, consoles in sections.items() for console in consoles} if isinstance(sections, dict) else set(sections) ) + # A reviewed parent under a maker the pages carry is a section even with + # no folder of its own: the Atom's sets sit under bios/Arcade, and the + # system pages file them under Acorn (generate_site.arcade_board_sections). + makers = {mfr for mfr, _console in pairs} + pairs |= {parent for parent in SYSTEM_PARENTS.values() + if parent[0] in makers and parent[0] not in _PROGRAM_TOPS} from sitestarlight import _unique, toc_slug _console_sections.clear() @@ -759,7 +820,7 @@ def _heading_name(mfr: str, console: str) -> str: from common import GAME_DATA_TOPS if mfr in GAME_DATA_TOPS: - return console + return SECTION_LABELS.get((mfr, console), console) if console.casefold().startswith(mfr.split()[0].casefold()): return console return f"{mfr} {console}" @@ -816,7 +877,6 @@ SYSTEM_NAMES: dict[str, str] = { "atari-tt": "Atari TT", "atari-zn1": "Atari ZN-1", "atlus-zn1": "Atlus ZN-1", - "atom": "Acorn Atom", "bandai-playdia": "Bandai Playdia", "bandai-wswan": "Bandai WonderSwan", "bandai-wswanc": "Bandai WonderSwan Color", @@ -1281,6 +1341,12 @@ SYSTEM_PARENTS: dict[str, tuple[str, str]] = { "philips-vg5k": ("Philips", "VG5000"), "vg5k": ("Philips", "VG5000"), "videobrain": ("VideoBrain", "Family Computer"), + # Computers whose sets sit under bios/Arcade with MAME's: the Atom has + # no folder of its own, its section holds those sets. The MZ-2000 and + # MZ-2500 sets join the MZ ones. + "atom": ("Acorn", "Atom"), + "mz2000": ("Sharp", "MZ"), + "mz2500": ("Sharp", "MZ"), # Files kept at the maker's root, so each file is a heading of its own # on a page that is the machine's: the first one leads there. "galaksija": ("Galaksija", "Galaksija"), @@ -1307,7 +1373,7 @@ SYSTEM_PARENTS: dict[str, tuple[str, str]] = { "bbc-master-et": ("Other", "beebem"), "cave-story": ("Other", "NXEngine"), "dos": ("DOS", "DOSBox"), - "scummvm": ("Other", "ScummVM"), + "scummvm": ("ScummVM", "ScummVM"), "sinclair-zx-spectrum-next": ("Other", "cspect"), "solarus": ("Game Engines", "Solarus"), "sunplus-spmp8000": ("Other", "spmp8000emu"), @@ -1317,8 +1383,20 @@ SYSTEM_PARENTS: dict[str, tuple[str, str]] = { "xrick": ("Other", "xrick"), "zc210": ("ZC", "ZC210"), # The collection's general arcade section, where naomi.zip, neogeo.zip - # and the other board sets sit. + # and the other board sets sit. A sound chip's set and a toy's stay in + # the Arcade folders holding them: neither is an arcade board. "arcade": ("Arcade", "Arcade"), + "miuchiz": ("Arcade", "MAME"), + "ym2608": ("Arcade", "Arcade"), +} + + +# A system id that names a family of arcade boards -> the board its link +# leads to. RetroArch's sega-dreamcast-arcade lists the NAOMI, NAOMI 2 and +# Atomiswave BIOS: the NAOMI page comes first, the Dreamcast section links +# the others. +BOARD_PARENTS: dict[str, str] = { + "sega-dreamcast-arcade": "sega-naomi", } diff --git a/tests/test_site_exports.py b/tests/test_site_exports.py index c19cc7b0..cae5191a 100644 --- a/tests/test_site_exports.py +++ b/tests/test_site_exports.py @@ -361,7 +361,8 @@ class AVariantShowsWhoPinsIt(unittest.TestCase): platforms={"c" * 40: {"batocera", "retrobat"}}, emulators={"a" * 40: {"mame"}}) page = generate_site.generate_system_page( "Entex", {"Adventure Vision": [primary, variant]}, consumers) - block = page[page.index("Variants (1)"):] + # Another revision of a set sits open beside it (round 5, 6.3). + block = page[page.index('id="sha1-' + "c" * 12 + '"'):] self.assertIn("
Collection path
bios/Entex/Adventure Vision/" ".variants/advision.zip.1c086064", block) self.assertIn('Batocera', block) diff --git a/tests/test_site_r3_nav.py b/tests/test_site_r3_nav.py index 5a7ef2b9..04c75b1c 100644 --- a/tests/test_site_r3_nav.py +++ b/tests/test_site_r3_nav.py @@ -84,10 +84,36 @@ class MetaFields(unittest.TestCase): match = re.search(r'data-pagefind-meta="aliases:([^"]*)"', text) self.assertIsNotNone(match, "no aliases field on the PlayStation page") self.assertIn("psx", unescape(match[1]).split()) - dreamcast = (DOCS / "systems" / "sega-dreamcast.md").read_text(encoding="utf-8") + # The Dreamcast section's page: its own, or Sega's when it fits there. + own = DOCS / "systems" / "sega-dreamcast.md" + dreamcast = (own if own.is_file() else DOCS / "systems" / "sega.md").read_text( + encoding="utf-8") self.assertRegex(dreamcast, r'data-pagefind-meta="aliases:[^"]*\bdc\b') +class SystemPartsSitUnderTheirMaker(unittest.TestCase): + """The crumbs say where a system part sits, read from its parent: field. + The "Part of" line under them said it again (round 5, 6.9).""" + + def test_every_part_names_its_maker_as_parent(self) -> None: + pages = sorted((DOCS / "systems").glob("*.md")) + if not pages: + self.skipTest("site pages not generated") + makers = {page.stem for page in pages} + orphans = [] + for page in pages: + text = page.read_text(encoding="utf-8") + front = text.split("\n---\n", 1)[0] + parent = re.search(r"^parent:\n label: .+\n link: /retrobios/systems/([a-z0-9-]+)/$", + front, re.M) + if parent is None: + continue + if parent[1] not in makers: + orphans.append(page.name) + self.assertNotIn("\nPart of [", text, page.name) + self.assertEqual(orphans, []) + + class MovedPlatformPages(unittest.TestCase): def test_a_setups_list_for_one_console_is_a_reference(self) -> None: source = Path(generate_site.__file__).read_text(encoding="utf-8") diff --git a/tests/test_site_r3_p0.py b/tests/test_site_r3_p0.py index 58712552..6128d71c 100644 --- a/tests/test_site_r3_p0.py +++ b/tests/test_site_r3_p0.py @@ -42,7 +42,7 @@ UNLINKED: dict[str, str] = { "namco-nb1": "Arcade > FBNeo", "omicron": "Other > omicron", "rpgmaker": "RPG Maker > easyrpg", - "sdlpal": "Other > sdlpal", + "sdlpal": "the SDLPAL section under its own top, named after the program", "sega-system16b": "Arcade > FBNeo, and isgsm.zip is also the ISG Selection Master's", "smbx": "Game Engines > TheXTech", "thomson-mo6": "Other > Clock Signal", @@ -50,7 +50,6 @@ UNLINKED: dict[str, str] = { # Files filed with another machine: a link would name the wrong one. "commodore-c64-supercpu": "VICE's tree under the C128", "emerson-arcadia-2001": "ar_bios.zip is filed with the Saturn", - "sega-dreamcast-arcade": "NAOMI, NAOMI 2 and Atomiswave sets, three boards", "spectravideo": "the SVI ROMs are filed with the MSX", "taito-type-x": "mame2010 files taitogn.zip, the G-NET BIOS, under this id", "vvvvvv": "SDL's controller database", @@ -105,7 +104,7 @@ class SystemLinks(unittest.TestCase): load_platform_config, resolve_local_file, ) - from generate_site import _copy_places, _group_by_manufacturer + from generate_site import _group_by_manufacturer, arcade_board_sections with open("database.json", encoding="utf-8") as handle: db = json.load(handle) @@ -117,8 +116,9 @@ class SystemLinks(unittest.TestCase): # (round 4, 6.4) and record it. The built pages are checked by # the round 4 gate. siterender.reset_console_pages() - for board in siterender.arcade_boards(): - siterender.record_board_section(board) + # The sections as the system pages hold them: each board, the + # sets a reviewed machine owns under that machine. + cls.sections, _installs = arcade_board_sections(cls.manufacturers, {}) platforms = [ load_platform_config(name, "platforms") for name in list_registered_platforms("platforms", include_archived=True) @@ -141,7 +141,12 @@ class SystemLinks(unittest.TestCase): for owner in owners: entries.setdefault(owner, []).append(fe) - places = _copy_places(db) + # Where each content has a card. + places: dict[str, set[tuple[str, str, str]]] = {} + for mfr, consoles in cls.sections.items(): + for console, files in consoles.items(): + for f in files: + places.setdefault(f["sha1"], set()).add((mfr, console, f["name"])) by_path = {entry.get("path"): sha1 for sha1, entry in db["files"].items()} # Id -> the (manufacturer, console) sections holding its files. cls.held: dict[str, set[tuple[str, str]]] = {} @@ -210,7 +215,7 @@ class SystemLinks(unittest.TestCase): def test_parents_are_sections(self) -> None: sections = { (mfr, console) - for mfr, consoles in self.manufacturers.items() for console in consoles + for mfr, consoles in self.sections.items() for console in consoles } self.assertEqual( sorted(k for k, v in siterender.SYSTEM_PARENTS.items() if v not in sections), []) diff --git a/tests/test_site_r4_p0.py b/tests/test_site_r4_p0.py index 866c9bb6..8b025903 100644 --- a/tests/test_site_r4_p0.py +++ b/tests/test_site_r4_p0.py @@ -59,7 +59,6 @@ STAYS_PLAIN = { "fpinball": "scripten.exe, filed with Windows", "pv1000": "only MAME's software list pv1000.xml", "vc4000": "only MAME's software list vc4000.xml", - "sega-dreamcast-arcade": "NAOMI, NAOMI 2 and Atomiswave sets, three boards", "spectravideo": "the SVI ROMs are filed with the MSX", "vgmplay": "sound chip sets, no board", "vvvvvv": "SDL's controller database", @@ -139,9 +138,11 @@ class Boards(unittest.TestCase): slug = siterender.board_slug(board) self.assertTrue(slug) self.assertEqual(slug, toc_slug(name)) + # The board's page is its section: no heading repeats its + # title (round 5, 6.8). self.assertEqual( siterender.console_href(*siterender.system_section(board)), - f"systems/arcade-{slug}.md#{slug}") + f"systems/arcade-{slug}.md") def test_aliases_share_a_board(self) -> None: """Every id of one board's name leads to one section.""" @@ -171,7 +172,7 @@ class Boards(unittest.TestCase): def test_parents_named_by_the_plan(self) -> None: expect = { "dos": ("dos", "DOSBox"), - "scummvm": ("other", "ScummVM"), + "scummvm": ("scummvm", "ScummVM"), "tyrian": ("other", "opentyrian"), "bbc-master-128": ("other", "beebem"), } diff --git a/tests/test_site_r4_systems.py b/tests/test_site_r4_systems.py index 40529d3f..75c5a9c2 100644 --- a/tests/test_site_r4_systems.py +++ b/tests/test_site_r4_systems.py @@ -161,10 +161,12 @@ class EachArcadeBoardHasASection(unittest.TestCase): """6.4: the Arcade page was cut by storage folder.""" def test_each_board_heads_its_page(self) -> None: + # The page's title names the board, no h2 says it again (round 5, 6.8). for name in BOARDS: slug = toc_slug(name) html = _page(DIST / "systems" / f"arcade-{slug}" / "index.html") - self.assertRegex(html, rf']*id="{re.escape(slug)}"', name) + self.assertRegex(html, rf"]*>Arcade: {re.escape(name)}", name) + self.assertNotRegex(html, rf"]*>{re.escape(name)}<", name) def test_every_board_on_an_emulator_systems_row_is_a_link(self) -> None: if not (DIST / "emulators").is_dir(): @@ -223,7 +225,10 @@ class PartPagesKnowWhereTheyAre(unittest.TestCase): orphans, numbered = [], [] for page in sorted((CONTENT / "systems").glob("*.md*")): text = page.read_text(encoding="utf-8") - if "Part of " in text and "\nparent:" not in text.split("\n---\n", 1)[0]: + # A part is titled ": " or " files a to b". + title = re.search(r"^title: (.*)$", text, re.M) + part = title and (": " in title[1] or re.search(r" files .+ to ", title[1])) + if part and "\nparent:" not in text.split("\n---\n", 1)[0]: orphans.append(page.name) if re.search(r"^title: .*part \d+ of \d+", text, re.M | re.I): numbered.append(page.name)