Fixed issues with windowed mode and fullscreen.
This commit is contained in:
@@ -45,6 +45,10 @@ RETIRED_POST_AUTH_GAME_DLL_SHA256 = (
|
||||
"93cd27284eb294753da1111c99bfc9689f82b6d4a59302aee9fdbd8cb52452dd"
|
||||
)
|
||||
RUNTIME_VIEW_CONFIGURATION_VERSION = 2
|
||||
WINDOWED_GAME_CONFIG_VALUES = (
|
||||
(b"windowed", b"Windowed", b"1"),
|
||||
(b"startmaximized", b"StartMaximized", b"False"),
|
||||
)
|
||||
|
||||
|
||||
class OverlayError(RuntimeError):
|
||||
@@ -175,7 +179,8 @@ def prepare_runtime_view(
|
||||
_validate_game_tree(source)
|
||||
_validate_runtime_target(paths)
|
||||
game_save = _prepare_persistent_save(paths, source)
|
||||
reused = _reuse_runtime_view(paths, source, game_save)
|
||||
game_config = _prepare_persistent_game_config(paths, source)
|
||||
reused = _reuse_runtime_view(paths, source, game_save, game_config)
|
||||
if reused is not None:
|
||||
return reused
|
||||
input_identities = _runtime_input_identities(paths, source)
|
||||
@@ -197,6 +202,7 @@ def prepare_runtime_view(
|
||||
inspection,
|
||||
action,
|
||||
game_save,
|
||||
game_config,
|
||||
binary_action,
|
||||
game_exe_source,
|
||||
game_dll_source,
|
||||
@@ -211,7 +217,7 @@ def prepare_runtime_view(
|
||||
links_created = 0
|
||||
try:
|
||||
build.mkdir(parents=True, exist_ok=False)
|
||||
top_level_exceptions = {"game", "save"}
|
||||
top_level_exceptions = {"game", "game.ini", "save"}
|
||||
for name, binary_source in (
|
||||
("Game.exe", game_exe_source),
|
||||
("game.dll", game_dll_source),
|
||||
@@ -225,6 +231,8 @@ def prepare_runtime_view(
|
||||
)
|
||||
(build / "save").symlink_to(game_save, target_is_directory=True)
|
||||
links_created += 1
|
||||
(build / "game.ini").symlink_to(game_config)
|
||||
links_created += 1
|
||||
if game_exe_source != source / "Game.exe":
|
||||
(build / "Game.exe").symlink_to(game_exe_source)
|
||||
links_created += 1
|
||||
@@ -510,6 +518,108 @@ def _prepare_persistent_save(paths: AppPaths, source: Path) -> Path:
|
||||
return target
|
||||
|
||||
|
||||
def _prepare_persistent_game_config(paths: AppPaths, source: Path) -> Path:
|
||||
"""Keep game settings writable while forcing a true windowed startup."""
|
||||
|
||||
target = paths.game_config
|
||||
if target.is_symlink():
|
||||
raise OverlayError(f"persistent game config must not be a symbolic link: {target}")
|
||||
if target.exists() and not target.is_file():
|
||||
raise OverlayError(f"persistent game config is not a file: {target}")
|
||||
source_config = target if target.is_file() else source / "game.ini"
|
||||
try:
|
||||
content = source_config.read_bytes()
|
||||
mode = source_config.stat().st_mode & 0o777
|
||||
except OSError as exc:
|
||||
raise OverlayError(f"could not read game config {source_config}: {exc}") from exc
|
||||
windowed_content = _force_windowed_game_config(content)
|
||||
if source_config == target and windowed_content == content:
|
||||
return target
|
||||
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
temporary = target.with_name(f".{target.name}.{uuid.uuid4().hex}.tmp")
|
||||
try:
|
||||
with temporary.open("xb") as handle:
|
||||
handle.write(windowed_content)
|
||||
handle.flush()
|
||||
os.fsync(handle.fileno())
|
||||
temporary.chmod(mode)
|
||||
temporary.replace(target)
|
||||
except BaseException:
|
||||
temporary.unlink(missing_ok=True)
|
||||
raise
|
||||
return target
|
||||
|
||||
|
||||
def _force_windowed_game_config(content: bytes) -> bytes:
|
||||
"""Set only the two Screen keys that can trigger a desktop-sized backbuffer."""
|
||||
|
||||
if b"\x00" in content:
|
||||
raise OverlayError("game.ini uses an unsupported encoding")
|
||||
newline = b"\r\n" if b"\r\n" in content else b"\n"
|
||||
desired = {
|
||||
key: (display_name, value)
|
||||
for key, display_name, value in WINDOWED_GAME_CONFIG_VALUES
|
||||
}
|
||||
result: list[bytes] = []
|
||||
in_screen = False
|
||||
found_screen = False
|
||||
found_settings: set[bytes] = set()
|
||||
|
||||
def append_missing_settings() -> None:
|
||||
if result and not result[-1].endswith((b"\r", b"\n")):
|
||||
result[-1] += newline
|
||||
for key, display_name, value in WINDOWED_GAME_CONFIG_VALUES:
|
||||
if key in found_settings:
|
||||
continue
|
||||
result.append(display_name + b"=" + value + newline)
|
||||
found_settings.add(key)
|
||||
|
||||
for line in content.splitlines(keepends=True):
|
||||
if line.endswith(b"\r\n"):
|
||||
body, line_ending = line[:-2], b"\r\n"
|
||||
elif line.endswith((b"\r", b"\n")):
|
||||
body, line_ending = line[:-1], line[-1:]
|
||||
else:
|
||||
body, line_ending = line, b""
|
||||
stripped = body.strip()
|
||||
if stripped.startswith(b"[") and b"]" in stripped:
|
||||
if in_screen:
|
||||
append_missing_settings()
|
||||
section_end = stripped.find(b"]")
|
||||
in_screen = stripped[1:section_end].strip().lower() == b"screen"
|
||||
found_screen = found_screen or in_screen
|
||||
if in_screen and b"=" in body and not stripped.startswith((b";", b"#")):
|
||||
equals = body.find(b"=")
|
||||
key = body[:equals].strip().lower()
|
||||
setting = desired.get(key)
|
||||
if setting is not None:
|
||||
leading_value_space = body[equals + 1 :]
|
||||
leading_value_space = leading_value_space[
|
||||
: len(leading_value_space) - len(leading_value_space.lstrip(b" \t"))
|
||||
]
|
||||
result.append(
|
||||
body[: equals + 1]
|
||||
+ leading_value_space
|
||||
+ setting[1]
|
||||
+ line_ending
|
||||
)
|
||||
found_settings.add(key)
|
||||
continue
|
||||
result.append(line)
|
||||
|
||||
if in_screen:
|
||||
append_missing_settings()
|
||||
elif not found_screen:
|
||||
if result and not result[-1].endswith((b"\r", b"\n")):
|
||||
result[-1] += newline
|
||||
if result and result[-1].strip():
|
||||
result.append(newline)
|
||||
result.append(b"[Screen]" + newline)
|
||||
append_missing_settings()
|
||||
return b"".join(result)
|
||||
|
||||
|
||||
def _reject_save_symlinks(root: Path) -> None:
|
||||
for directory, directory_names, file_names in os.walk(root, followlinks=False):
|
||||
parent = Path(directory)
|
||||
@@ -617,6 +727,7 @@ def _reuse_legacy_runtime_view(
|
||||
inspection: SceneInspection,
|
||||
scene_action: str,
|
||||
game_save: Path,
|
||||
game_config: Path,
|
||||
binary_action: str,
|
||||
game_exe_source: Path,
|
||||
game_dll_source: Path,
|
||||
@@ -640,10 +751,16 @@ def _reuse_legacy_runtime_view(
|
||||
return None
|
||||
if legacy.game_dll_sha256 != source_binary_hashes.get("game.dll"):
|
||||
return None
|
||||
_migrate_runtime_game_config_link(
|
||||
paths.runtime_view / "game.ini",
|
||||
source / "game.ini",
|
||||
game_config,
|
||||
)
|
||||
if not _runtime_layout_ready(
|
||||
paths,
|
||||
source,
|
||||
game_save,
|
||||
game_config,
|
||||
game_exe_source,
|
||||
game_dll_source,
|
||||
legacy.links_created,
|
||||
@@ -691,16 +808,23 @@ def _reuse_runtime_view(
|
||||
paths: AppPaths,
|
||||
source: Path,
|
||||
game_save: Path,
|
||||
game_config: Path,
|
||||
) -> OverlayResult | None:
|
||||
result = _matching_cached_runtime_result(paths, source)
|
||||
if result is None:
|
||||
return None
|
||||
game_exe_source = source / "Game.exe"
|
||||
game_dll_source = source / "game.dll"
|
||||
_migrate_runtime_game_config_link(
|
||||
paths.runtime_view / "game.ini",
|
||||
source / "game.ini",
|
||||
game_config,
|
||||
)
|
||||
if not _runtime_layout_ready(
|
||||
paths,
|
||||
source,
|
||||
game_save,
|
||||
game_config,
|
||||
game_exe_source,
|
||||
game_dll_source,
|
||||
result.links_created,
|
||||
@@ -724,6 +848,7 @@ def _runtime_layout_ready(
|
||||
paths: AppPaths,
|
||||
source: Path,
|
||||
game_save: Path,
|
||||
game_config: Path,
|
||||
game_exe_source: Path,
|
||||
game_dll_source: Path,
|
||||
expected_links: int,
|
||||
@@ -731,8 +856,8 @@ def _runtime_layout_ready(
|
||||
runtime = paths.runtime_view
|
||||
if runtime.is_symlink() or not runtime.is_dir():
|
||||
return False
|
||||
top_level_exceptions = {"game", "save"}
|
||||
top_level_special = {"game", "save"}
|
||||
top_level_exceptions = {"game", "game.ini", "save"}
|
||||
top_level_special = {"game", "game.ini", "save"}
|
||||
for name, binary_source in (
|
||||
("Game.exe", game_exe_source),
|
||||
("game.dll", game_dll_source),
|
||||
@@ -750,6 +875,8 @@ def _runtime_layout_ready(
|
||||
return False
|
||||
if not _symlink_points_to(runtime / "save", game_save):
|
||||
return False
|
||||
if not _symlink_points_to(runtime / "game.ini", game_config):
|
||||
return False
|
||||
if game_exe_source != source / "Game.exe" and not _symlink_points_to(
|
||||
runtime / "Game.exe",
|
||||
game_exe_source,
|
||||
@@ -789,7 +916,7 @@ def _runtime_layout_ready(
|
||||
runtime_scene = maps_target / "WhisperingVale.rovug"
|
||||
if runtime_scene.is_symlink() or not runtime_scene.is_file():
|
||||
return False
|
||||
actual_links = top_level_links + game_links + maps_links + 1
|
||||
actual_links = top_level_links + game_links + maps_links + 2
|
||||
if game_exe_source != source / "Game.exe":
|
||||
actual_links += 1
|
||||
if game_dll_source != source / "game.dll":
|
||||
@@ -830,6 +957,27 @@ def _symlink_points_to(link: Path, expected_target: Path) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _migrate_runtime_game_config_link(
|
||||
link: Path,
|
||||
source_config: Path,
|
||||
persistent_config: Path,
|
||||
) -> bool:
|
||||
"""Atomically redirect the previous source link without rebuilding the view."""
|
||||
|
||||
if _symlink_points_to(link, persistent_config):
|
||||
return True
|
||||
if not _symlink_points_to(link, source_config):
|
||||
return False
|
||||
temporary = link.with_name(f".{link.name}.{uuid.uuid4().hex}.tmp")
|
||||
try:
|
||||
temporary.symlink_to(persistent_config)
|
||||
temporary.replace(link)
|
||||
except OSError:
|
||||
temporary.unlink(missing_ok=True)
|
||||
return False
|
||||
return _symlink_points_to(link, persistent_config)
|
||||
|
||||
|
||||
def _read_overlay_result(path: Path) -> OverlayResult | None:
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
Reference in New Issue
Block a user