mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-10 21:43:23 -05:00
167 lines
5.6 KiB
Python
167 lines
5.6 KiB
Python
"""Contract shared by the platform exporters.
|
|
|
|
An exporter answers one question: what would this platform's own file look
|
|
like if it were corrected. It is handed the platform's file when we have it,
|
|
because several of these formats carry code, and a generator that emits only
|
|
the data block hands the maintainer something that no longer runs.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from abc import ABC, abstractmethod
|
|
|
|
from .baseline import NativeFile, NativeSystem, Report
|
|
|
|
|
|
class BaseExporter(ABC):
|
|
"""Base class for writing a platform's own BIOS file, corrected."""
|
|
|
|
@staticmethod
|
|
@abstractmethod
|
|
def platform_name() -> str:
|
|
"""Return the platform identifier this exporter targets."""
|
|
|
|
@staticmethod
|
|
@abstractmethod
|
|
def native_filename() -> str:
|
|
"""Return the name the platform gives this file."""
|
|
|
|
@staticmethod
|
|
def carries() -> frozenset[str]:
|
|
"""Fields this format has somewhere to put.
|
|
|
|
A correction the file cannot state is not a correction it delivers,
|
|
and counting it would announce a change the maintainer will not find
|
|
in the diff.
|
|
"""
|
|
return frozenset({"md5"})
|
|
|
|
@staticmethod
|
|
def native_sources() -> dict[str, str]:
|
|
"""Return {relative path: URL} of the originals this exporter patches.
|
|
|
|
An exporter that can build its file from nothing returns an empty
|
|
mapping. One whose format carries code cannot, and names the file it
|
|
needs so the caller can fetch it.
|
|
"""
|
|
return {}
|
|
|
|
@staticmethod
|
|
def needs_original() -> bool:
|
|
"""Whether a faithful export requires the platform's own file."""
|
|
return False
|
|
|
|
@staticmethod
|
|
def may_write_nothing() -> bool:
|
|
"""Whether producing no file is an outcome rather than a failure.
|
|
|
|
True only where the unit is a correction rather than a document:
|
|
RetroPie has no BIOS list to rewrite, so a run with nothing to
|
|
correct writes nothing and is right to.
|
|
"""
|
|
return False
|
|
|
|
@abstractmethod
|
|
def render(
|
|
self,
|
|
systems: dict[str, NativeSystem],
|
|
report: Report,
|
|
originals: dict[str, str],
|
|
scraped: dict | None = None,
|
|
) -> dict[str, str]:
|
|
"""Return {relative output path: file content}."""
|
|
|
|
@abstractmethod
|
|
def validate(
|
|
self,
|
|
systems: dict[str, NativeSystem],
|
|
produced: dict[str, str],
|
|
) -> list[str]:
|
|
"""Check the produced files against what was asked of them."""
|
|
|
|
# Shared helpers
|
|
|
|
@classmethod
|
|
def exportable(
|
|
cls,
|
|
systems: dict[str, NativeSystem],
|
|
require: str = "",
|
|
) -> list[tuple[NativeSystem, list[NativeFile]]]:
|
|
"""Systems paired with the files this format can actually carry.
|
|
|
|
`require` names a hash the format cannot omit. An entry the format
|
|
has no way to express is dropped here and counted by the caller,
|
|
never written as a half entry the platform would read as a file that
|
|
can never verify.
|
|
"""
|
|
result: list[tuple[NativeSystem, list[NativeFile]]] = []
|
|
for system in systems.values():
|
|
files = [fe for fe in system.files if fe.name and cls.writable(fe, require)]
|
|
if files:
|
|
result.append((system, files))
|
|
return result
|
|
|
|
@staticmethod
|
|
def requires() -> str:
|
|
"""The hash this format cannot write a new entry without."""
|
|
return ""
|
|
|
|
@staticmethod
|
|
def can_add() -> bool:
|
|
"""Whether a file the platform does not declare can be stated at all.
|
|
|
|
False where an entry is a declaration in code rather than a row:
|
|
BizHawk wires each firmware into an option list and a status that
|
|
only its source expresses, and writing C# for one is not something
|
|
an exporter can do safely.
|
|
"""
|
|
return True
|
|
|
|
@classmethod
|
|
def writable(cls, fe: NativeFile, require: str = "") -> bool:
|
|
"""Whether this format can carry the entry.
|
|
|
|
An entry the platform already declares is always carried, hash or
|
|
no hash: it is in their file today, and an export that drops it
|
|
hands back a file poorer than the one it corrects. The requirement
|
|
only gates what we would be adding.
|
|
"""
|
|
if fe.platform is not None:
|
|
return True
|
|
if not cls.can_add():
|
|
return False
|
|
require = require or cls.requires()
|
|
return not require or bool(fe.hash(require))
|
|
|
|
def outcome(
|
|
self,
|
|
systems: dict[str, NativeSystem],
|
|
produced: dict[str, str],
|
|
) -> str | None:
|
|
"""What this export did, when counting file entries would not say it.
|
|
|
|
Most formats state one entry per file, so the caller's own count is
|
|
the answer. RetroPie states a sentence per package, and a count of
|
|
file entries would describe work it did not do.
|
|
"""
|
|
return None
|
|
|
|
@staticmethod
|
|
def display_name(system: NativeSystem) -> str:
|
|
"""The name the platform shows for a system."""
|
|
if system.name:
|
|
return system.name
|
|
for fe in system.files:
|
|
native = fe.native("native_name", "")
|
|
if native:
|
|
return str(native)
|
|
_UPPER = {
|
|
"3do", "cdi", "cpc", "cps1", "cps2", "cps3", "dos", "gba", "gbc",
|
|
"hle", "msx", "nes", "nds", "ngp", "psp", "psx", "sms", "snes",
|
|
"stv", "tvc", "vb", "zx",
|
|
}
|
|
parts = system.native_id.replace("-", " ").replace("_", " ").split()
|
|
return " ".join(
|
|
p.upper() if p.lower() in _UPPER else p.capitalize() for p in parts
|
|
)
|