1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
|
"""OptiScaler configuration migration and default handling."""
import re
from pathlib import Path
from typing import Callable
Logger = Callable[[str], None]
def _split_override_key(raw_key: str) -> tuple[str | None, str]:
key = str(raw_key).strip()
if "." in key:
section, section_key = key.split(".", 1)
section = section.strip()
section_key = section_key.strip()
if section and section_key:
return section, section_key
return None, key
def apply_overrides(ini_file: Path, overrides: dict, logger: Logger | None = None) -> bool:
"""Upsert INI keys without disturbing unrelated game configuration."""
if not overrides:
return True
try:
content = ini_file.read_text(encoding="utf-8", errors="replace")
newline = "\r\n" if "\r\n" in content else "\n"
lines = content.splitlines(keepends=True)
section_pattern = re.compile(r"^\s*\[(?P<section>[^\]]+)\]\s*$")
def ensure_trailing_newline() -> None:
if lines and not lines[-1].endswith(("\n", "\r")):
lines[-1] += newline
def upsert(section: str | None, key: str, value: str) -> None:
replacement = f"{key}={value}"
key_pattern = re.compile(rf"^(\s*{re.escape(key)}\s*)=.*$")
if section is None:
for index, line in enumerate(lines):
if key_pattern.match(line):
ending = "\r\n" if line.endswith("\r\n") else ("\n" if line.endswith("\n") else newline)
lines[index] = f"{replacement}{ending}"
return
ensure_trailing_newline()
lines.append(f"{replacement}{newline}")
return
in_section = False
insert_at = None
for index, line in enumerate(lines):
match = section_pattern.match(line.strip())
if match:
if in_section:
insert_at = index
break
if match.group("section") == section:
in_section = True
continue
if in_section and key_pattern.match(line):
ending = "\r\n" if line.endswith("\r\n") else ("\n" if line.endswith("\n") else newline)
lines[index] = f"{replacement}{ending}"
return
if in_section:
if insert_at is None:
ensure_trailing_newline()
insert_at = len(lines)
lines.insert(insert_at, f"{replacement}{newline}")
return
ensure_trailing_newline()
if lines and lines[-1].strip():
lines.append(newline)
lines.extend((f"[{section}]{newline}", f"{replacement}{newline}"))
for raw_key, raw_value in overrides.items():
section, key = _split_override_key(str(raw_key))
if key:
upsert(section, key, str(raw_value).strip())
ini_file.write_text("".join(lines), encoding="utf-8")
return True
except Exception as exc:
if logger:
logger(f"Failed to apply OptiScaler.ini overrides to {ini_file}: {exc}")
return False
def migrate(ini_file: Path, logger: Logger | None = None) -> bool:
"""Migrate the pre-0.9 ``FGType`` setting to split input/output keys."""
try:
if not ini_file.exists():
return False
content = ini_file.read_text(encoding="utf-8", errors="replace")
match = re.search(r"^FGType\s*=\s*(\S+)", content, re.MULTILINE)
if not match:
return True
value = match.group(1)
if re.search(r"^FGInput\s*=", content, re.MULTILINE):
updated = re.sub(r"^FGType\s*=\s*\S+\n?", "", content, flags=re.MULTILINE)
message = f"Removed stale FGType from {ini_file}"
else:
updated = re.sub(
r"^FGType\s*=\s*\S+",
f"FGInput={value}\nFGOutput={value}",
content,
flags=re.MULTILINE,
)
message = f"Migrated FGType={value} in {ini_file}"
if updated != content:
ini_file.write_text(updated, encoding="utf-8")
if logger:
logger(message)
return True
except Exception as exc:
if logger:
logger(f"Failed to migrate OptiScaler.ini: {exc}")
return False
def disable_hq_font_auto(ini_file: Path, logger: Logger | None = None) -> bool:
"""Avoid the HQ font auto mode, which is unreliable under Proton."""
try:
if not ini_file.exists():
return False
content = ini_file.read_text(encoding="utf-8", errors="replace")
updated = re.sub(r"UseHQFont\s*=\s*auto", "UseHQFont=false", content)
if updated != content:
ini_file.write_text(updated, encoding="utf-8")
return True
except Exception as exc:
if logger:
logger(f"Failed to update HQ font setting in {ini_file}: {exc}")
return False
def configure(
ini_file: Path,
framegen_backend: str,
variant_overrides: dict | None = None,
logger: Logger | None = None,
) -> bool:
"""Apply safe defaults and the selected frame-generation backend."""
if not ini_file.exists():
return False
backend = framegen_backend if framegen_backend in {"auto", "nukems"} else "auto"
overrides = {
"FGInput": backend,
"FGOutput": backend,
"Fsr4Update": "true",
"LoadAsiPlugins": "true",
}
if variant_overrides:
overrides.update(variant_overrides)
migrated = migrate(ini_file, logger)
hq_updated = disable_hq_font_auto(ini_file, logger)
configured = apply_overrides(ini_file, overrides, logger)
return migrated and hq_updated and configured
def detect_backend(ini_file: Path) -> str:
"""Return the configured backend when both FG keys agree."""
try:
content = ini_file.read_text(encoding="utf-8", errors="replace")
except OSError:
return "auto"
values = [
match.group(1).strip().lower()
for match in re.finditer(r"^FG(?:Input|Output)\s*=\s*(\S+)", content, re.MULTILINE)
]
if values and all(value == "nukems" for value in values):
return "nukems"
return "auto"
|