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:
Abdessamad Derraz committed 2026-10-21 10:42:09 +02:00
1 parent fd1d1b7b96
commit 448540dced
4 files changed
+634 -7

No files matched your search

-7
View File
@@ -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
+255
View File
@@ -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
+229
View File
@@ -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,
+150
View File
@@ -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}