mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-23 20:29:06 -05:00
feat: extract retrobat guarded bios ini writes
emulatorLauncher writes a BIOS path into an emulator's configuration only when the file exists: string x = Path.Combine(bios, "name"); if (File.Exists(x)) ini.WriteValue(section, key, x). The extractor now records each such write as a fixed_name launcher record, with the enclosing if and early-return conditions, and a key that receives the bios folder itself as passes_path. A path reassigned before the check, or a write under a loop or an else, is not read. Generators read at 48cdc616: KegaFusion (SCD and 32X keys, GenesisBIOS given the folder), Jgenesis, MelonDS (DS), Mupen64, Project64, mGBA. The RetroBat paragraph of the Kega Fusion profile now lives in the launcher layer.
This commit is contained in:
1 parent
fd1d1b7b96
commit
448540dced
4 files changed
+634
-7
No files matched your search
@@ -37,13 +37,6 @@ notes: |
|
||||
file names the binary fixes. Each file is opened with fopen "rb" and read
|
||||
with a fixed count, no length, checksum or header is compared.
|
||||
|
||||
RetroBat's generator writes SCDEURBIOS, SCDUSABIOS and SCDJAPBIOS as
|
||||
bios_CD_E.bin, bios_CD_U.bin and bios_CD_J.bin, and the three 32X keys as
|
||||
32X_G_BIOS.BIN, 32X_M_BIOS.BIN and 32X_S_BIOS.BIN in its bios folder, each
|
||||
only when the file exists. GenesisBIOS receives the bios folder itself, so
|
||||
the fopen fails and no TMSS ROM is loaded. The 8-bit keys are not written.
|
||||
ref: emulatorlauncher Generators/KegaFusion.Generator.cs:99-128
|
||||
|
||||
CDTray.raw, Cursor_8.raw, STATIC_8.RAW and gg_nor.all are looked up by name
|
||||
in an archive embedded in the executable (0x47a530), not on disk. Plugins
|
||||
(*.rpi) and the KGV1 video codec ship with the program. Fusion.ini, battery
|
||||
|
||||
@@ -3238,6 +3238,261 @@ launchers:
|
||||
fails_when_missing: not checked by the launcher
|
||||
depends_on_option: duck_bios
|
||||
source_ref: "emulatorLauncher/Generators/Duckstation.Generator.cs:204-209, .emulationstation/es_features.cfg:11638-11646"
|
||||
- emulator: jgenesis
|
||||
build: standalone
|
||||
kind: fixed_name
|
||||
names:
|
||||
- gba_bios.bin
|
||||
case_insensitive: true
|
||||
search_dir: bios
|
||||
passes_to: "jgenesis-config.toml [game_boy_advance] bios_path, only if the file\
|
||||
\ exists"
|
||||
fails_when_missing: bios_path not written
|
||||
depends_on_option: system == "game_boy_advance"
|
||||
source_ref: "emulatorLauncher/Generators/Jgenesis.Generator.cs:257-263"
|
||||
- emulator: jgenesis
|
||||
build: standalone
|
||||
kind: fixed_name
|
||||
names:
|
||||
- bios_CD_U.bin
|
||||
case_insensitive: true
|
||||
search_dir: bios
|
||||
passes_to: "jgenesis-config.toml [sega_cd] bios_path, only if the file exists"
|
||||
fails_when_missing: bios_path not written
|
||||
depends_on_option: system == "genesis" || system == "sega_cd" || system == "sega_32x",
|
||||
system == "sega_cd"
|
||||
source_ref: "emulatorLauncher/Generators/Jgenesis.Generator.cs:282-283, 317, 323-324"
|
||||
- emulator: jgenesis
|
||||
build: standalone
|
||||
kind: fixed_name
|
||||
names:
|
||||
- bios_CD_J.bin
|
||||
case_insensitive: true
|
||||
search_dir: bios
|
||||
passes_to: "jgenesis-config.toml [sega_cd] jp_bios_path, only if the file exists"
|
||||
fails_when_missing: jp_bios_path not written
|
||||
depends_on_option: system == "genesis" || system == "sega_cd" || system == "sega_32x",
|
||||
system == "sega_cd"
|
||||
source_ref: "emulatorLauncher/Generators/Jgenesis.Generator.cs:282-283, 318, 325-326"
|
||||
- emulator: jgenesis
|
||||
build: standalone
|
||||
kind: fixed_name
|
||||
names:
|
||||
- bios_CD_E.bin
|
||||
case_insensitive: true
|
||||
search_dir: bios
|
||||
passes_to: "jgenesis-config.toml [sega_cd] eu_bios_path, only if the file exists"
|
||||
fails_when_missing: eu_bios_path not written
|
||||
depends_on_option: system == "genesis" || system == "sega_cd" || system == "sega_32x",
|
||||
system == "sega_cd"
|
||||
source_ref: "emulatorLauncher/Generators/Jgenesis.Generator.cs:282-283, 319, 327-328"
|
||||
- emulator: kega-fusion
|
||||
build: standalone
|
||||
kind: fixed_name
|
||||
names:
|
||||
- bios_CD_E.bin
|
||||
case_insensitive: true
|
||||
search_dir: bios
|
||||
passes_to: Fusion.ini SCDEURBIOS, only if the file exists
|
||||
fails_when_missing: SCDEURBIOS not written
|
||||
source_ref: "emulatorLauncher/Generators/KegaFusion.Generator.cs:106-108"
|
||||
- emulator: kega-fusion
|
||||
build: standalone
|
||||
kind: fixed_name
|
||||
names:
|
||||
- bios_CD_U.bin
|
||||
case_insensitive: true
|
||||
search_dir: bios
|
||||
passes_to: Fusion.ini SCDUSABIOS, only if the file exists
|
||||
fails_when_missing: SCDUSABIOS not written
|
||||
source_ref: "emulatorLauncher/Generators/KegaFusion.Generator.cs:110-112"
|
||||
- emulator: kega-fusion
|
||||
build: standalone
|
||||
kind: fixed_name
|
||||
names:
|
||||
- bios_CD_J.bin
|
||||
case_insensitive: true
|
||||
search_dir: bios
|
||||
passes_to: Fusion.ini SCDJAPBIOS, only if the file exists
|
||||
fails_when_missing: SCDJAPBIOS not written
|
||||
source_ref: "emulatorLauncher/Generators/KegaFusion.Generator.cs:114-116"
|
||||
- emulator: kega-fusion
|
||||
build: standalone
|
||||
kind: fixed_name
|
||||
names:
|
||||
- 32X_G_BIOS.BIN
|
||||
case_insensitive: true
|
||||
search_dir: bios
|
||||
passes_to: Fusion.ini 32XM68KBIOS, only if the file exists
|
||||
fails_when_missing: 32XM68KBIOS not written
|
||||
source_ref: "emulatorLauncher/Generators/KegaFusion.Generator.cs:118-120"
|
||||
- emulator: kega-fusion
|
||||
build: standalone
|
||||
kind: fixed_name
|
||||
names:
|
||||
- 32X_M_BIOS.BIN
|
||||
case_insensitive: true
|
||||
search_dir: bios
|
||||
passes_to: Fusion.ini 32XMSH2BIOS, only if the file exists
|
||||
fails_when_missing: 32XMSH2BIOS not written
|
||||
source_ref: "emulatorLauncher/Generators/KegaFusion.Generator.cs:122-124"
|
||||
- emulator: kega-fusion
|
||||
build: standalone
|
||||
kind: fixed_name
|
||||
names:
|
||||
- 32X_S_BIOS.BIN
|
||||
case_insensitive: true
|
||||
search_dir: bios
|
||||
passes_to: Fusion.ini 32XSSH2BIOS, only if the file exists
|
||||
fails_when_missing: 32XSSH2BIOS not written
|
||||
source_ref: "emulatorLauncher/Generators/KegaFusion.Generator.cs:126-128"
|
||||
- emulator: kega-fusion
|
||||
build: standalone
|
||||
kind: passes_path
|
||||
names: []
|
||||
search_dir: bios
|
||||
passes_to: Fusion.ini GenesisBIOS = bios
|
||||
fails_when_missing: false
|
||||
source_ref: "emulatorLauncher/Generators/KegaFusion.Generator.cs:99, 103"
|
||||
- emulator: melonds
|
||||
build: standalone
|
||||
kind: fixed_name
|
||||
names:
|
||||
- firmware.bin
|
||||
case_insensitive: true
|
||||
search_dir: bios
|
||||
passes_to: "melonDS.toml [DS] FirmwarePath, only if the file exists"
|
||||
fails_when_missing: FirmwarePath not written
|
||||
depends_on_option: melonds_externalBIOS on
|
||||
source_ref: "emulatorLauncher/Generators/MelonDS.Generator.cs:167, 174-175"
|
||||
- emulator: melonds
|
||||
build: standalone
|
||||
kind: fixed_name
|
||||
names:
|
||||
- bios7.bin
|
||||
case_insensitive: true
|
||||
search_dir: bios
|
||||
passes_to: "melonDS.toml [DS] BIOS7Path, only if the file exists"
|
||||
fails_when_missing: BIOS7Path not written
|
||||
depends_on_option: melonds_externalBIOS on
|
||||
source_ref: "emulatorLauncher/Generators/MelonDS.Generator.cs:168, 177-178"
|
||||
- emulator: melonds
|
||||
build: standalone
|
||||
kind: fixed_name
|
||||
names:
|
||||
- bios9.bin
|
||||
case_insensitive: true
|
||||
search_dir: bios
|
||||
passes_to: "melonDS.toml [DS] BIOS9Path, only if the file exists"
|
||||
fails_when_missing: BIOS9Path not written
|
||||
depends_on_option: melonds_externalBIOS on
|
||||
source_ref: "emulatorLauncher/Generators/MelonDS.Generator.cs:169, 180-181"
|
||||
- emulator: mupen64
|
||||
build: standalone
|
||||
kind: fixed_name
|
||||
names:
|
||||
- IPL_JAP.n64
|
||||
case_insensitive: true
|
||||
search_dir: bios/Mupen64plus
|
||||
passes_to: "Config/mupen64plus.cfg [Rosalie's Mupen GUI Core 64DD] 64DD_JapaneseIPL,\
|
||||
\ only if the file exists"
|
||||
fails_when_missing: 64DD_JapaneseIPL not written
|
||||
source_ref: "emulatorLauncher/Generators/Mupen64.Generator.cs:228-230"
|
||||
- emulator: mupen64
|
||||
build: standalone
|
||||
kind: fixed_name
|
||||
names:
|
||||
- IPL_USA.n64
|
||||
case_insensitive: true
|
||||
search_dir: bios/Mupen64plus
|
||||
passes_to: "Config/mupen64plus.cfg [Rosalie's Mupen GUI Core 64DD] 64DD_AmericanIPL,\
|
||||
\ only if the file exists"
|
||||
fails_when_missing: 64DD_AmericanIPL not written
|
||||
source_ref: "emulatorLauncher/Generators/Mupen64.Generator.cs:232-234"
|
||||
- emulator: mupen64
|
||||
build: standalone
|
||||
kind: fixed_name
|
||||
names:
|
||||
- IPL_DEV.n64
|
||||
case_insensitive: true
|
||||
search_dir: bios/Mupen64plus
|
||||
passes_to: "Config/mupen64plus.cfg [Rosalie's Mupen GUI Core 64DD] 64DD_DevelopmentIPL,\
|
||||
\ only if the file exists"
|
||||
fails_when_missing: 64DD_DevelopmentIPL not written
|
||||
source_ref: "emulatorLauncher/Generators/Mupen64.Generator.cs:236-238"
|
||||
- emulator: project64
|
||||
build: standalone
|
||||
kind: fixed_name
|
||||
names:
|
||||
- IPL_JAP.n64
|
||||
case_insensitive: true
|
||||
search_dir: bios/Mupen64plus
|
||||
passes_to: "Config/Project64.cfg [Settings] Disk IPL ROM Path, only if the file\
|
||||
\ exists"
|
||||
fails_when_missing: Disk IPL ROM Path not written
|
||||
source_ref: "emulatorLauncher/Generators/Project64.Generator.cs:91-93"
|
||||
- emulator: project64
|
||||
build: standalone
|
||||
kind: fixed_name
|
||||
names:
|
||||
- IPL_USA.n64
|
||||
case_insensitive: true
|
||||
search_dir: bios/Mupen64plus
|
||||
passes_to: "Config/Project64.cfg [Settings] Disk IPL USA ROM Path, only if the file\
|
||||
\ exists"
|
||||
fails_when_missing: Disk IPL USA ROM Path not written
|
||||
source_ref: "emulatorLauncher/Generators/Project64.Generator.cs:95-97"
|
||||
- emulator: project64
|
||||
build: standalone
|
||||
kind: fixed_name
|
||||
names:
|
||||
- IPL_DEV.n64
|
||||
case_insensitive: true
|
||||
search_dir: bios/Mupen64plus
|
||||
passes_to: "Config/Project64.cfg [Settings] Disk IPL TOOL ROM Path, only if the\
|
||||
\ file exists"
|
||||
fails_when_missing: Disk IPL TOOL ROM Path not written
|
||||
source_ref: "emulatorLauncher/Generators/Project64.Generator.cs:99-101"
|
||||
- emulator: mgba
|
||||
build: standalone
|
||||
kind: fixed_name
|
||||
names:
|
||||
- gb_bios.bin
|
||||
case_insensitive: true
|
||||
search_dir: bios
|
||||
passes_to: "config.ini [ports.qt] gb.bios, only if the file exists"
|
||||
fails_when_missing: gb.bios not written
|
||||
source_ref: "emulatorLauncher/Generators/mGBA.Generator.cs:112-114"
|
||||
- emulator: mgba
|
||||
build: standalone
|
||||
kind: fixed_name
|
||||
names:
|
||||
- sgb2_boot.bin
|
||||
case_insensitive: true
|
||||
search_dir: bios
|
||||
passes_to: "config.ini [ports.qt] sgb.bios, only if the file exists"
|
||||
fails_when_missing: sgb.bios not written
|
||||
source_ref: "emulatorLauncher/Generators/mGBA.Generator.cs:116-118"
|
||||
- emulator: mgba
|
||||
build: standalone
|
||||
kind: fixed_name
|
||||
names:
|
||||
- gbc_bios.bin
|
||||
case_insensitive: true
|
||||
search_dir: bios
|
||||
passes_to: "config.ini [ports.qt] gbc.bios, only if the file exists"
|
||||
fails_when_missing: gbc.bios not written
|
||||
source_ref: "emulatorLauncher/Generators/mGBA.Generator.cs:120-122"
|
||||
- emulator: mgba
|
||||
build: standalone
|
||||
kind: fixed_name
|
||||
names:
|
||||
- gba_bios.bin
|
||||
case_insensitive: true
|
||||
search_dir: bios
|
||||
passes_to: "config.ini [ports.qt] gba.bios, only if the file exists"
|
||||
fails_when_missing: gba.bios not written
|
||||
source_ref: "emulatorLauncher/Generators/mGBA.Generator.cs:124-126"
|
||||
- emulator: hatari
|
||||
build: standalone
|
||||
kind: fixed_name
|
||||
|
||||
@@ -23,13 +23,19 @@ SOURCE_FILES = (
|
||||
f"{GENERATORS}/Bizhawk.Generator.cs",
|
||||
f"{GENERATORS}/Duckstation.Generator.cs",
|
||||
f"{GENERATORS}/Hatari.Generator.cs",
|
||||
f"{GENERATORS}/Jgenesis.Generator.cs",
|
||||
f"{GENERATORS}/KegaFusion.Generator.cs",
|
||||
f"{GENERATORS}/LibRetro.CoreOptions.cs",
|
||||
f"{GENERATORS}/Mednafen.Generator.cs",
|
||||
f"{GENERATORS}/MelonDS.Generator.cs",
|
||||
f"{GENERATORS}/Mesen.Generator.cs",
|
||||
f"{GENERATORS}/Mupen64.Generator.cs",
|
||||
f"{GENERATORS}/OpenMSX.Generator.cs",
|
||||
f"{GENERATORS}/Pcsx2.Generator.cs",
|
||||
f"{GENERATORS}/Project64.Generator.cs",
|
||||
f"{GENERATORS}/Uae.Generator.cs",
|
||||
f"{GENERATORS}/Xemu.Generator.cs",
|
||||
f"{GENERATORS}/mGBA.Generator.cs",
|
||||
FEATURES,
|
||||
)
|
||||
|
||||
@@ -54,6 +60,7 @@ class Source:
|
||||
self.text = text
|
||||
self._starts = [0] + [m.end() for m in re.finditer(r"\n", text)]
|
||||
self._blocks: list[tuple[int, int]] | None = None
|
||||
self._ends: list[int] | None = None
|
||||
|
||||
def line(self, offset: int) -> int:
|
||||
return bisect.bisect_right(self._starts, offset)
|
||||
@@ -107,6 +114,15 @@ class Source:
|
||||
raise LauncherPatternMissing(self.path, f"block around line {self.line(offset)}")
|
||||
return max(holding)
|
||||
|
||||
def header(self, block: tuple[int, int]) -> str:
|
||||
"""The statement that owns a block's opening brace, comments removed."""
|
||||
if self._ends is None:
|
||||
self._ends = [o for o, c in _code_chars(self.text) if c in ";{}"]
|
||||
previous = bisect.bisect_left(self._ends, block[0]) - 1
|
||||
start = self._ends[previous] + 1 if previous >= 0 else 0
|
||||
text = re.sub(r"//[^\n]*|/\*.*?\*/", "", self.text[start : block[0]], flags=re.S)
|
||||
return " ".join(text.split())
|
||||
|
||||
def statement_span(self, block: tuple[int, int]) -> tuple[int, int]:
|
||||
"""Lines of a block, from the statement that owns its opening brace."""
|
||||
first, last = self.lines(*block)
|
||||
@@ -1169,10 +1185,223 @@ def duckstation(sources: dict[str, Source]) -> list[Record]:
|
||||
]
|
||||
|
||||
|
||||
# Generators that write each BIOS path into the emulator's configuration
|
||||
# only when the file exists: string x = Path.Combine(bios, ..., "name");
|
||||
# if (File.Exists(x)) ini.WriteValue("section", "key", x).
|
||||
GUARDED_WRITES = {
|
||||
f"{GENERATORS}/Jgenesis.Generator.cs": "jgenesis",
|
||||
f"{GENERATORS}/KegaFusion.Generator.cs": "kega-fusion",
|
||||
f"{GENERATORS}/MelonDS.Generator.cs": "melonds",
|
||||
f"{GENERATORS}/Mupen64.Generator.cs": "mupen64",
|
||||
f"{GENERATORS}/Project64.Generator.cs": "project64",
|
||||
f"{GENERATORS}/mGBA.Generator.cs": "mgba",
|
||||
}
|
||||
|
||||
_METHOD = re.compile(
|
||||
r"(?:(?:public|private|protected|internal|static|override|virtual)\s+)+[\w<>\[\],. ]+\s\w+\s*\("
|
||||
)
|
||||
_TRANSPARENT = re.compile(r"(?:using\s*\(.*\)|try)")
|
||||
_IF = re.compile(r"(?:else\s+)?if\s*\((.*)\)")
|
||||
_GUARD = re.compile(
|
||||
r'if \(File\.Exists\((\w+)\)\)\s*(\w+)\.WriteValue\("([^"]*)", "([^"]+)", ([^;\n]+)\);'
|
||||
)
|
||||
_BIOS_ROOT = r'(?:Program\.)?AppConfig\.GetFullPath\("bios"\)'
|
||||
|
||||
|
||||
def _negate(condition: str) -> str:
|
||||
"""The condition under which an early return is not taken.
|
||||
|
||||
A conjunction of inequalities becomes the disjunction of equalities,
|
||||
anything else is wrapped in not (...).
|
||||
"""
|
||||
tests = condition.split(" && ")
|
||||
compared = [re.fullmatch(rf"(\w+) != ({STRING})", t) for t in tests]
|
||||
if all(compared):
|
||||
return " || ".join(f"{m.group(1)} == {m.group(2)}" for m in compared)
|
||||
return f"not ({condition})"
|
||||
|
||||
|
||||
Reach = tuple[tuple[int, int], list[str], list[tuple[int, int]]]
|
||||
|
||||
|
||||
def _conditions(src: Source, offset: int) -> Reach | None:
|
||||
"""The method holding offset, the conditions under which it is reached,
|
||||
and the lines of the early returns among them.
|
||||
|
||||
Each enclosing if adds its condition, and an early return at the top of
|
||||
the method adds the condition that does not return. None when a block in
|
||||
between is not an if, a using or a try: a loop or an else would need its
|
||||
own reading.
|
||||
"""
|
||||
enclosing: list[str] = []
|
||||
for block in sorted((b for b in src.blocks() if b[0] < offset < b[1]), reverse=True):
|
||||
header = src.header(block)
|
||||
if _METHOD.match(header):
|
||||
returns, spans = [], []
|
||||
for found in re.finditer(r"\bif \(", src.text[block[0] : offset]):
|
||||
paren = block[0] + found.end() - 1
|
||||
if src.enclosing(paren) != block:
|
||||
continue
|
||||
cond = _call_args(src, paren)
|
||||
tail = re.compile(r"\s*return\b[^;]*;").match(src.text, paren + len(cond) + 2)
|
||||
if tail:
|
||||
returns.append(_negate(" ".join(cond.split())))
|
||||
spans.append(src.lines(block[0] + found.start(), tail.end()))
|
||||
return block, returns + enclosing[::-1], spans
|
||||
condition = _IF.fullmatch(header)
|
||||
if condition:
|
||||
text = " ".join(condition.group(1).split())
|
||||
switch = re.fullmatch(
|
||||
r'SystemConfig\.isOptSet\("(\w+)"\) && SystemConfig\.getOptBoolean\("\1"\)', text
|
||||
)
|
||||
enclosing.append(f"{switch.group(1)} on" if switch else text)
|
||||
elif not _TRANSPARENT.fullmatch(header):
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _config_name(src: Source, method: tuple[int, int], writer: str) -> str:
|
||||
"""The file the writer variable opens, read in its method or else in the file."""
|
||||
opener = r"(\w+)\s*=\s*(?:\w+\.FromFile|new \w+)\((\w+)"
|
||||
declared = {
|
||||
m.group(1): "/".join(_strings(m.group(2)))
|
||||
for m in re.finditer(
|
||||
r'string (\w+) = Path\.Combine\(path, ((?:"[^"]+"(?:, )?)+)\);', src.text
|
||||
)
|
||||
}
|
||||
local = [
|
||||
m.group(2)
|
||||
for m in re.finditer(opener, src.text[method[0] : method[1]])
|
||||
if m.group(1) == writer and m.group(2) in declared
|
||||
]
|
||||
if local:
|
||||
return declared[local[0]]
|
||||
files = {declared[m.group(2)] for m in re.finditer(opener, src.text) if m.group(2) in declared}
|
||||
if len(files) != 1:
|
||||
raise LauncherPatternMissing(src.path, f"configuration file opened as {writer}")
|
||||
return files.pop()
|
||||
|
||||
|
||||
def _root_writes(src: Source, emulator: str) -> list[Record]:
|
||||
"""Configuration keys that receive the bios folder itself."""
|
||||
records = []
|
||||
for root in re.finditer(rf"string (\w+) = {_BIOS_ROOT};", src.text):
|
||||
root_reach = _conditions(src, root.start())
|
||||
if root_reach is None:
|
||||
continue
|
||||
method = root_reach[0]
|
||||
for write in re.compile(
|
||||
rf'(\w+)\.WriteValue\("([^"]*)", "([^"]+)", {root.group(1)}\);'
|
||||
).finditer(src.text, root.end(), method[1]):
|
||||
reached = _conditions(src, write.start())
|
||||
if reached is None:
|
||||
continue
|
||||
writer, section, key = write.groups()
|
||||
target = f"[{section}] {key}" if section else key
|
||||
records.append(
|
||||
_record(
|
||||
emulator,
|
||||
"standalone",
|
||||
"passes_path",
|
||||
[],
|
||||
source_ref(
|
||||
(
|
||||
src.path,
|
||||
[
|
||||
*reached[2],
|
||||
src.lines(root.start(), root.end()),
|
||||
src.lines(write.start(), write.end()),
|
||||
],
|
||||
)
|
||||
),
|
||||
search_dir="bios",
|
||||
passes_to=f"{_config_name(src, method, writer)} {target} = bios",
|
||||
fails_when_missing=False,
|
||||
depends_on_option=", ".join(reached[1]) or None,
|
||||
)
|
||||
)
|
||||
return records
|
||||
|
||||
|
||||
def _guarded_write(src: Source, emulator: str, guard: re.Match[str]) -> Record | None:
|
||||
"""The record of one guarded write, None when its shape is another one.
|
||||
|
||||
The written value must be the checked path itself (quoted or with its
|
||||
separators replaced), the path a literal under the bios folder that
|
||||
nothing reassigns before the check, and every block around the write an
|
||||
if, a using or a try.
|
||||
"""
|
||||
var, writer, section, key, value = guard.groups()
|
||||
if not re.fullmatch(
|
||||
rf"(?:{STRING} \+ )?{var}(?:\.Replace\({STRING}, {STRING}\))?(?: \+ {STRING})?",
|
||||
value.strip(),
|
||||
):
|
||||
return None
|
||||
reached = _conditions(src, guard.start())
|
||||
if reached is None:
|
||||
return None
|
||||
method, conditions, returns = reached
|
||||
decls = list(
|
||||
re.compile(
|
||||
rf"string {var} = Path\.Combine\((\w+|{_BIOS_ROOT}), ((?:{STRING}, )*{STRING})\);"
|
||||
).finditer(src.text, method[0], guard.start())
|
||||
)
|
||||
if not decls or re.search(rf"\b{var}\s*=[^=]", src.text[decls[-1].end() : guard.start()]):
|
||||
return None
|
||||
decl = decls[-1]
|
||||
root = decl.group(1)
|
||||
if not re.fullmatch(_BIOS_ROOT, root) and not re.search(
|
||||
rf"string {root} = {_BIOS_ROOT};", src.text[method[0] : decl.start()]
|
||||
):
|
||||
return None
|
||||
*folders, name = _strings(decl.group(2))
|
||||
target = f"[{section}] {key}" if section else key
|
||||
return _record(
|
||||
emulator,
|
||||
"standalone",
|
||||
"fixed_name",
|
||||
[name],
|
||||
source_ref(
|
||||
(
|
||||
src.path,
|
||||
[
|
||||
*returns,
|
||||
src.lines(decl.start(), decl.end()),
|
||||
src.lines(guard.start(), guard.end()),
|
||||
],
|
||||
)
|
||||
),
|
||||
case_insensitive=True,
|
||||
search_dir="/".join(["bios", *folders]),
|
||||
passes_to=f"{_config_name(src, method, writer)} {target}, only if the file exists",
|
||||
fails_when_missing=f"{key} not written",
|
||||
depends_on_option=", ".join(conditions) or None,
|
||||
)
|
||||
|
||||
|
||||
def guarded_writes_in(src: Source, emulator: str) -> list[Record]:
|
||||
"""Every guarded write of one generator, then the keys given the bios folder."""
|
||||
found = [_guarded_write(src, emulator, guard) for guard in _GUARD.finditer(src.text)]
|
||||
records = [record for record in found if record is not None]
|
||||
if not records:
|
||||
raise LauncherPatternMissing(src.path, "File.Exists guarded WriteValue")
|
||||
return records + _root_writes(src, emulator)
|
||||
|
||||
|
||||
def guarded_writes(sources: dict[str, Source]) -> list[Record]:
|
||||
"""One fixed name per configuration key, written only when the file exists."""
|
||||
return [
|
||||
record
|
||||
for path, emulator in GUARDED_WRITES.items()
|
||||
for record in guarded_writes_in(sources[path], emulator)
|
||||
]
|
||||
|
||||
|
||||
EXTRACTORS: tuple[Callable[[dict[str, Source]], list[Record]], ...] = (
|
||||
amiberry,
|
||||
ares,
|
||||
duckstation,
|
||||
guarded_writes,
|
||||
hatari,
|
||||
hatari_libretro,
|
||||
mesen,
|
||||
|
||||
@@ -201,6 +201,94 @@ HATARI_FEATURES = """
|
||||
"""
|
||||
|
||||
|
||||
KEGA = """
|
||||
private void SetupConfiguration(string path, string system)
|
||||
{
|
||||
string iniFile = Path.Combine(path, "Fusion.ini");
|
||||
|
||||
using (var ini = IniFile.FromFile(iniFile))
|
||||
{
|
||||
string biosPath = AppConfig.GetFullPath("bios");
|
||||
if (!Directory.Exists(biosPath)) try { Directory.CreateDirectory(biosPath); }
|
||||
catch { }
|
||||
|
||||
ini.WriteValue("", "GenesisBIOS", biosPath);
|
||||
|
||||
// BIOS
|
||||
string euBios = Path.Combine(biosPath, "bios_CD_E.bin");
|
||||
if (File.Exists(euBios))
|
||||
ini.WriteValue("", "SCDEURBIOS", euBios);
|
||||
|
||||
string bios32Xg = Path.Combine(biosPath, "32X_G_BIOS.BIN");
|
||||
if (File.Exists(bios32Xg))
|
||||
ini.WriteValue("", "32XM68KBIOS", bios32Xg);
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
JGENESIS = """
|
||||
private void SetupTomlConfiguration(string path)
|
||||
{
|
||||
string settingsFile = Path.Combine(path, "jgenesis-config.toml");
|
||||
using (IniFileJGenesis ini = new IniFileJGenesis(settingsFile, IniOptionsJGenesis.UseSpaces))
|
||||
{
|
||||
ConfigureGenesis(ini, system);
|
||||
}
|
||||
}
|
||||
|
||||
private void ConfigureGenesis(IniFileJGenesis ini, string system)
|
||||
{
|
||||
if (system != "genesis" && system != "sega_cd")
|
||||
return;
|
||||
|
||||
if (system == "sega_cd")
|
||||
{
|
||||
string segaCdBios = Path.Combine(AppConfig.GetFullPath("bios"), "bios_CD_U.bin");
|
||||
|
||||
ini.WriteValue("sega_cd", "per_region_bios", "true");
|
||||
|
||||
if (File.Exists(segaCdBios))
|
||||
ini.WriteValue("sega_cd", "bios_path", "'" + segaCdBios + "'");
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
MELONDS = """
|
||||
private void SetupConfiguration(string path, string rom)
|
||||
{
|
||||
string settingsFile = Path.Combine(path, "melonDS.toml");
|
||||
|
||||
using (IniTomlFile ini = IniTomlFile.FromFile(settingsFile, IniTomlOptions.UseSpaces))
|
||||
{
|
||||
if (SystemConfig.isOptSet("melonds_externalBIOS") && SystemConfig.getOptBoolean("melonds_externalBIOS"))
|
||||
{
|
||||
string bios7 = Path.Combine(AppConfig.GetFullPath("bios"), "bios7.bin");
|
||||
|
||||
if (File.Exists(bios7))
|
||||
ini.WriteValue("DS", "BIOS7Path", "\\"" + bios7.Replace("\\\\", "/") + "\\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
# Kronos: the path is replaced by an option before the check, so the name
|
||||
# checked is not the literal.
|
||||
REASSIGNED = """
|
||||
private void SetupConfiguration(string path)
|
||||
{
|
||||
string conf = Path.Combine(path, "kronos.ini");
|
||||
using (var ini = IniFile.FromFile(conf))
|
||||
{
|
||||
string bios = Path.Combine(AppConfig.GetFullPath("bios"), "saturn_bios.bin");
|
||||
if (SystemConfig.isOptSet("kronos_force_bios"))
|
||||
bios = Path.Combine(AppConfig.GetFullPath("bios"), SystemConfig["kronos_force_bios"]);
|
||||
if (File.Exists(bios))
|
||||
ini.WriteValue("1.0", "General\\\\Bios", bios.Replace("\\\\", "/"));
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def _psx_sources() -> dict[str, rl.Source]:
|
||||
return _sources(
|
||||
**{
|
||||
@@ -314,6 +402,68 @@ class NamesInLauncherOrder(unittest.TestCase):
|
||||
self.assertEqual(option["search_dir"], "bios/hatari/tos")
|
||||
|
||||
|
||||
def _one(text: str, emulator: str, path: str = f"{G}/X.Generator.cs") -> list[dict]:
|
||||
return rl.guarded_writes_in(_sources(**{path: text})[path], emulator)
|
||||
|
||||
|
||||
class GuardedConfigWrites(unittest.TestCase):
|
||||
def test_kega_fusion_one_record_per_key(self):
|
||||
records = _one(KEGA, "kega-fusion")
|
||||
fixed = [r for r in records if r["kind"] == "fixed_name"]
|
||||
self.assertEqual(
|
||||
[(r["names"], r["passes_to"], r["fails_when_missing"]) for r in fixed],
|
||||
[
|
||||
(["bios_CD_E.bin"], "Fusion.ini SCDEURBIOS, only if the file exists",
|
||||
"SCDEURBIOS not written"),
|
||||
(["32X_G_BIOS.BIN"], "Fusion.ini 32XM68KBIOS, only if the file exists",
|
||||
"32XM68KBIOS not written"),
|
||||
],
|
||||
)
|
||||
self.assertEqual(fixed[0]["source_ref"], f"{G}/X.Generator.cs:14-16")
|
||||
self.assertTrue(all(r["search_dir"] == "bios" and r["case_insensitive"] for r in fixed))
|
||||
self.assertNotIn("depends_on_option", fixed[0])
|
||||
|
||||
def test_kega_fusion_genesis_key_receives_the_folder(self):
|
||||
(folder,) = [r for r in _one(KEGA, "kega-fusion") if r["kind"] == "passes_path"]
|
||||
self.assertEqual(folder["names"], [])
|
||||
self.assertEqual(folder["passes_to"], "Fusion.ini GenesisBIOS = bios")
|
||||
self.assertEqual(folder["source_ref"], f"{G}/X.Generator.cs:7, 11")
|
||||
|
||||
def test_early_return_and_enclosing_if_are_conditions(self):
|
||||
(record,) = _one(JGENESIS, "jgenesis")
|
||||
self.assertEqual(
|
||||
record["depends_on_option"],
|
||||
'system == "genesis" || system == "sega_cd", system == "sega_cd"',
|
||||
)
|
||||
self.assertEqual(
|
||||
record["passes_to"], "jgenesis-config.toml [sega_cd] bios_path, only if the file exists"
|
||||
)
|
||||
self.assertEqual(record["source_ref"], f"{G}/X.Generator.cs:12-13, 17, 21-22")
|
||||
|
||||
def test_option_switch_is_named(self):
|
||||
(record,) = _one(MELONDS, "melonds")
|
||||
self.assertEqual(record["names"], ["bios7.bin"])
|
||||
self.assertEqual(record["depends_on_option"], "melonds_externalBIOS on")
|
||||
self.assertEqual(
|
||||
record["passes_to"], "melonDS.toml [DS] BIOS7Path, only if the file exists"
|
||||
)
|
||||
|
||||
def test_reassigned_path_is_not_a_fixed_name(self):
|
||||
with self.assertRaises(rl.LauncherPatternMissing):
|
||||
_one(REASSIGNED, "kronos")
|
||||
|
||||
def test_write_inside_a_loop_is_not_read(self):
|
||||
looped = KEGA.replace("// BIOS", "foreach (var x in xs)\n {").replace(
|
||||
'"32XM68KBIOS", bios32Xg);', '"32XM68KBIOS", bios32Xg);\n }'
|
||||
)
|
||||
with self.assertRaises(rl.LauncherPatternMissing):
|
||||
_one(looped, "kega-fusion")
|
||||
|
||||
def test_pinned_kega_fusion_generator_is_read(self):
|
||||
self.assertIn(f"{G}/KegaFusion.Generator.cs", rl.SOURCE_FILES)
|
||||
self.assertEqual(rl.GUARDED_WRITES[f"{G}/KegaFusion.Generator.cs"], "kega-fusion")
|
||||
|
||||
|
||||
class AbsentConstructStopsTheScrape(unittest.TestCase):
|
||||
def test_each_extractor_raises_on_an_empty_source(self):
|
||||
empty = {path: rl.Source(path, "") for path in rl.SOURCE_FILES}
|
||||
|
||||
Reference in new issue
Block a user